Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8490dc4362 | ||
|
|
77e6fbcb79 | ||
|
|
fefe46ac3a | ||
|
|
4ce884cbce | ||
|
|
1021e76185 | ||
|
|
dbd88bea70 | ||
|
|
c537c52c96 | ||
|
|
6f96d7a3fa | ||
|
|
7590b50801 | ||
|
|
bd5d189a8f | ||
|
|
89268f4a45 | ||
|
|
3637205622 | ||
|
|
b4f9f0deac | ||
|
|
ec0e663cca | ||
|
|
26356dc34a | ||
|
|
b928963675 | ||
|
|
8451b961ad |
No files matched your search
@@ -219,6 +219,13 @@ explicit path or endpoint and do not need it.
|
||||
delivery requires `--evidence_output`.
|
||||
See the
|
||||
[Rust binary guide](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
|
||||
- PE Authenticode certificate table: `inspect_pe_certificates` with the explicit
|
||||
local PE32/PE32+ file. This built-in target-free inspection requires no
|
||||
Windows SDK. Preserve WIN_CERTIFICATE ranges, types, payload SHA-256, and
|
||||
PKCS#7 signer/X.509 identity fields. A missing security directory is observed
|
||||
absence of that table, not proof the image is unsigned in every catalog sense.
|
||||
Signatures are not verified. See the
|
||||
[PE certificate guide](https://github.com/morluto/rea/blob/main/docs/pe-certificates.md).
|
||||
|
||||
- Explicit local SQLite database snapshot: `inspect_sqlite_database`. Inspect
|
||||
with Node.js 24.x >=24.15 or 26+ providing native SQLite limits. Read schema
|
||||
|
||||
@@ -22,10 +22,10 @@ requires complete application analysis; `source_paths: []` is invalid, not an
|
||||
unfiltered request. `detail: "summary"` reduces delivery
|
||||
and does not by itself reduce analysis work.
|
||||
|
||||
ASAR header/index admission is fixed at 16 MiB / 16777216 bytes for this
|
||||
release and has no runtime override. `source_paths`, `detail`, and
|
||||
`max_heap_mb` do not bypass or raise it. Analyze an extracted directory, or
|
||||
supply an archive whose header fits.
|
||||
ASAR header/index admission defaults to 16 MiB / 16777216 bytes;
|
||||
`REA_ASAR_MAX_HEADER_BYTES` (at most 256 MiB, set where REA starts) changes it.
|
||||
`source_paths`, `detail`, and `max_heap_mb` do not bypass or raise it. Or
|
||||
analyze an extracted directory.
|
||||
|
||||
Exact `#alias` specifiers resolve through the importer's
|
||||
enclosing package `imports` map to package-local files; nested `node_modules`
|
||||
|
||||
@@ -45,7 +45,8 @@ See [desktop installations](https://github.com/morluto/rea/blob/main/docs/deskto
|
||||
|
||||
## Go build metadata
|
||||
|
||||
Use `inspect_go_binary` for an explicit local ELF, PE or thin Mach-O when the
|
||||
Use `inspect_go_binary` for an explicit local ELF, PE or Mach-O (universal
|
||||
files need `architecture`) when the
|
||||
question concerns the embedded compiler version, module/dependency versions,
|
||||
local replacements or build settings. No `open_binary`, Go installation or
|
||||
target execution is required. The CLI equivalent is `inspect-go-binary PATH`.
|
||||
@@ -84,7 +85,8 @@ See [IL2CPP metadata](https://github.com/morluto/rea/blob/main/docs/il2cpp-metad
|
||||
|
||||
## Rust dependency metadata
|
||||
|
||||
Use `inspect_rust_binary` for an explicit local ELF, PE, or thin Mach-O when
|
||||
Use `inspect_rust_binary` for an explicit local ELF, PE, or Mach-O (universal
|
||||
files need `architecture`) when
|
||||
the question concerns the cargo-auditable crate list embedded in `.dep-v0`
|
||||
(Mach-O: `__DATA,.dep-v0`). No `open_binary`, Rust installation, or target
|
||||
execution is required. The CLI equivalent is `inspect-rust-binary PATH`.
|
||||
@@ -94,6 +96,19 @@ dependencies. An omitted package kind stays null. A rustc commit hash is
|
||||
inferred from a read-only `/rustc/<40-hex>/` path and does not by itself
|
||||
identify the compiler. See [Rust binaries](https://github.com/morluto/rea/blob/main/docs/rust-binaries.md).
|
||||
|
||||
## PE Authenticode certificate tables
|
||||
|
||||
Use `inspect_pe_certificates` for an explicit local PE32/PE32+ image when the
|
||||
question concerns the Authenticode certificate table, WIN_CERTIFICATE types, or
|
||||
embedded PKCS#7 signer and X.509 identity fields. No `open_binary`, Windows SDK,
|
||||
or target execution is required. The CLI equivalent is
|
||||
`inspect-pe-certificates PATH`. Preserve artifact SHA-256, table file ranges,
|
||||
payload hashes, and original validity-time spellings. A missing
|
||||
`IMAGE_DIRECTORY_ENTRY_SECURITY` directory is observed absence of that table,
|
||||
not proof the image is unsigned in every catalog or overlay sense. Signatures,
|
||||
chains, timestamps, and Authenticode digest coverage remain unobserved. See
|
||||
[PE Authenticode certificate tables](https://github.com/morluto/rea/blob/main/docs/pe-certificates.md).
|
||||
|
||||
## SQLite snapshots
|
||||
|
||||
Use `inspect_sqlite_database` with an explicit local database snapshot for its
|
||||
|
||||
@@ -191,6 +191,7 @@ Additional tools and host support depend on the target:
|
||||
| IL2CPP metadata | Unity global-metadata.dat assembly images, type and method names and index ranges | Built-in static decoding of versions 24, 27, 29, 31 and 39; no Unity required |
|
||||
| Godot PCK | Archive inventory and extraction, original header/directory facts and selected content identities | Built-in static formats 2, 3 and 4; no Godot required |
|
||||
| Rust binaries | Embedded cargo-auditable crate names, versions, sources and dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](docs/rust-binaries.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](docs/pe-certificates.md) |
|
||||
| WASM artifact | Validation, sections, exact import/export WAT forms and decoded WAT | Caller-supplied WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM bytecode | Dispatch selectors, byte offsets, inferred arguments and mutability | Local raw/hex carrier on Linux x64; [offline EVM guide](docs/evm-bytecode.md) |
|
||||
| SQLite databases | Tables, columns, indexes, views, triggers and selected ordinary-table rows | Local database/WAL snapshot; Node.js 24.x >=24.15 or 26+ with native SQLite limits |
|
||||
|
||||
@@ -32,6 +32,7 @@ export default defineConfig({
|
||||
{ text: "IL2CPP metadata", link: "/il2cpp-metadata" },
|
||||
{ text: "Model artifacts", link: "/model-artifacts" },
|
||||
{ text: "Rust dependencies", link: "/rust-binaries" },
|
||||
{ text: "PE certificates", link: "/pe-certificates" },
|
||||
{
|
||||
text: "JavaScript and Electron",
|
||||
link: "/javascript-artifact-reconstruction",
|
||||
|
||||
+12
@@ -155,6 +155,18 @@ dependency edges, and the section's file range. A missing `.dep-v0` section is
|
||||
null metadata, not proof that the binary has no dependencies. See
|
||||
[Rust binaries](rust-binaries.md).
|
||||
|
||||
Inspect a PE Authenticode certificate table without executing the image or
|
||||
verifying signatures:
|
||||
|
||||
```bash
|
||||
rea inspect-pe-certificates /absolute/path/to/application.exe --json
|
||||
```
|
||||
|
||||
The workflow reports `WIN_CERTIFICATE` entries and, for PKCS#7 signedData,
|
||||
embedded X.509 serial, issuer, subject, and validity fields. A missing security
|
||||
directory is observed absence of that table. See
|
||||
[PE Authenticode certificate tables](pe-certificates.md).
|
||||
|
||||
Configure [Hopper or Ghidra](installation.md#hopper), the
|
||||
[IDA adapter](ida-provider.md), or [Kuna](kuna-provider.md) before analyzing a
|
||||
native target. Substitute
|
||||
|
||||
@@ -247,8 +247,8 @@ capture provides them.
|
||||
|
||||
ASAR observation reads the header through one open file handle and validates its
|
||||
declared lengths against the archive size. Header/index admission is the same
|
||||
fixed 16 MiB / 16777216 byte budget used by static ASAR reads. This release has
|
||||
no runtime override. An oversized header is not parsed; selecting a member path
|
||||
does not bypass admission, and the page stays unauthorized. The header cache
|
||||
separately retains at most 16 MiB of already-admitted encoded headers across
|
||||
eight archives. Evicting that cache does not raise admission.
|
||||
budget used by static ASAR reads: 16 MiB / 16777216 bytes by default, or
|
||||
`REA_ASAR_MAX_HEADER_BYTES` (at most 256 MiB) when set. An oversized header is
|
||||
not parsed; selecting a member path does not bypass admission, and the page
|
||||
stays unauthorized. The header cache separately retains at most that many bytes
|
||||
of already-admitted encoded headers across eight archives. Evicting that cache does not raise admission.
|
||||
+15
-5
@@ -109,11 +109,21 @@ module record and no dependency records has an observed empty dependency list.
|
||||
|
||||
## Supported boundary
|
||||
|
||||
The built-in TypeScript reader supports ELF, PE and thin Mach-O images with Go's
|
||||
inline or historical pointer build-info encoding. Source ranges refer to the
|
||||
original stable file snapshot, including pointer-backed strings. It does not
|
||||
scan arbitrary byte strings as compiler metadata or interpret a fat Mach-O
|
||||
container as one selected architecture.
|
||||
The built-in TypeScript reader supports ELF, PE and thin Mach-O images, and one
|
||||
selected slice of a universal Mach-O, with Go's inline or historical pointer
|
||||
build-info encoding. Source ranges refer to the original stable file snapshot,
|
||||
including pointer-backed strings. It does not scan arbitrary byte strings as
|
||||
compiler metadata.
|
||||
|
||||
A universal (FAT) Mach-O needs `architecture` (`x86`, `x86_64`, `arm`
|
||||
or `arm64`; CLI `--architecture`). REA reads its own FAT table, without `lipo`,
|
||||
and validates the selected slice's range, alignment and Mach-O header CPU
|
||||
against the table. The result keeps the universal file as `artifact` (its
|
||||
SHA-256 and size), adds `selected_slice` with the slice offset, length, raw CPU
|
||||
type/subtype and the FAT table digest, and reports every source range as a
|
||||
file offset inside that slice. Without `architecture`, or when the file has no
|
||||
such slice or several, the request fails on `architecture` and lists the slices
|
||||
present. `architecture` on any other image is rejected rather than ignored.
|
||||
|
||||
For ELF images without a named build-info section, discovery checks each
|
||||
nonempty writable, non-executable load segment in program-header order. This
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
|
||||
| Task | Guides |
|
||||
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Identify a desktop installation | [Runtime candidates, evidence and follow-up files](desktop-applications.md) |
|
||||
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
|
||||
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-binaries.md), [Rust dependencies](rust-binaries.md), [PE certificates](pe-certificates.md), [offline ELF layout](binary-diagnostics.md), [recorded crashes](recorded-crashes.md) |
|
||||
| Select a native provider or import format | [Ghidra semantics](ghidra-provider.md), [Windows](windows-ghidra-p0.md), [DOS](ghidra-dos.md), [raw bytes](raw-binary.md), [MIPS](ghidra-mips.md), [PSP](ghidra-psp.md), [NativeAOT](ghidra-nativeaot.md), [IDA](ida-provider.md), [Kuna](kuna-provider.md), [JEB](jeb-analysis.md) |
|
||||
| Analyze JavaScript or Electron artifacts | [Static analysis](javascript-artifact-reconstruction.md), [tracing and comparisons](javascript-application-workflows.md), [application graph](javascript-application-graph.md), [source recovery](javascript-recovery.md) |
|
||||
| Compare static JavaScript with runtime observations | [Runtime attribution](javascript-runtime-observation.md), [reconciliation](javascript-runtime-reconciliation.md), [Electron observation](electron-observation.md), [Electron fuses](electron-fuses.md) |
|
||||
|
||||
@@ -106,13 +106,16 @@ only reduces delivery; source selection reduces text acquisition and analysis.
|
||||
Explicit native provider or snapshot routing rejects `--source-path` rather
|
||||
than ignoring it.
|
||||
|
||||
ASAR header/index admission is a fixed 16 MiB / 16777216 bytes for this
|
||||
release. There is no runtime override. The archive index is read before any
|
||||
member can be selected, so `source_paths`, `--source-path`, `detail:
|
||||
"summary"`, `max_heap_mb`, and available host memory do not bypass or raise
|
||||
it. When the header exceeds that allowance, the failure names this budget.
|
||||
Analyze an extracted application directory, which does not admit an ASAR
|
||||
header, or supply an archive whose header fits. Repeating the unchanged
|
||||
ASAR header/index admission defaults to 16 MiB / 16777216 bytes. Set
|
||||
`REA_ASAR_MAX_HEADER_BYTES` in the environment that starts REA to a decimal
|
||||
byte count of at most 268435456 (256 MiB) to raise or lower it; restart REA
|
||||
after changing it. Invalid values are configuration errors, never a silent
|
||||
fallback. The archive index is read before any member can be selected, so
|
||||
`source_paths`, `--source-path`, `detail: "summary"`, `max_heap_mb`, and
|
||||
available host memory do not bypass or raise it. When the header exceeds the
|
||||
effective allowance, the failure names it, whether it came from the default or
|
||||
the setting, and the setting's limits. Alternatively, analyze an extracted
|
||||
application directory, which does not admit an ASAR header. Repeating the unchanged
|
||||
request fails the same way.
|
||||
|
||||
## Read the analysis
|
||||
|
||||
@@ -108,9 +108,10 @@ nodes. REA evaluates compatible static candidates conservatively:
|
||||
6. Missing authorization mappings and incomplete or truncated input coverage
|
||||
remain unknown or truncated rather than becoming absence claims. A runtime
|
||||
location with no automatic artifact-root or caller-declared mapping into
|
||||
any layer, and no unique content match, is `unknown` with
|
||||
`no-authorized-location-mapping`, with or without captured bytes. Only a
|
||||
mapped location can be `unmatched` with `no-static-content-match`.
|
||||
any layer, and no content match, is `unknown` with
|
||||
`no-authorized-location-mapping`, with or without captured bytes; several
|
||||
content matches remain `ambiguous` under rule 4. Only a mapped location can
|
||||
be `unmatched` with `no-static-content-match`.
|
||||
|
||||
Every successful cross-layer `observed_as` edge has
|
||||
`authority: cross-layer-reconciliation` and `state: inferred`. The underlying
|
||||
|
||||
@@ -467,6 +467,14 @@ complete observation.
|
||||
vtable descriptors. Records include exact virtual addresses/file offsets and
|
||||
artifact evidence. Vtable indexes are metadata word offsets; witness indexes
|
||||
start after the conformance header. Names unavailable in metadata remain null.
|
||||
The byte reader holds the whole selected file in memory, so it admits files up
|
||||
to 64 MiB / 67108864 bytes by default. Select a larger budget with
|
||||
`max_input_bytes` (CLI `--max-input-bytes`), at most 1 GiB / 1073741824
|
||||
bytes. The whole file counts, including every slice of a universal Mach-O.
|
||||
Results record `input_admission` with the effective budget, its source
|
||||
(`default` or `caller`) and the bytes retained. A refusal names the observed
|
||||
file size, the effective budget and its source, and either the budget to
|
||||
select or, above 1 GiB, a deep analysis provider.
|
||||
The `swift_class_vtable_descriptors` coverage counters count examined vtable
|
||||
slots and successfully decoded slots, rather than type-section entries.
|
||||
The `objc_class_method_ivar_metadata` counters count admitted class and
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# PE Authenticode certificate tables
|
||||
|
||||
`inspect_pe_certificates` / `inspect-pe-certificates` answers whether one
|
||||
selected local PE32/PE32+ image declares an `IMAGE_DIRECTORY_ENTRY_SECURITY`
|
||||
table, which `WIN_CERTIFICATE` entries that table stores, and which PKCS#7
|
||||
signer and X.509 fields those entries carry. It reads the file without
|
||||
executing it and does not require a Windows SDK, catalog file, or disassembler.
|
||||
|
||||
```bash
|
||||
rea inspect-pe-certificates /absolute/path/to/application.exe --json
|
||||
```
|
||||
|
||||
The CLI resolves relative paths against the caller's working directory. MCP
|
||||
requires an absolute filesystem path and preserves that exact selection:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "inspect_pe_certificates",
|
||||
"arguments": { "path": "/artifacts/application.exe" }
|
||||
}
|
||||
```
|
||||
|
||||
## Observations
|
||||
|
||||
The inline Evidence result contains the artifact path, SHA-256, and byte length.
|
||||
`table` is the file-offset security directory, or `null` when the directory is
|
||||
absent or zero. Each `entries[]` item reports `WIN_CERTIFICATE` length,
|
||||
revision, type, and the payload SHA-256.
|
||||
|
||||
For `WIN_CERTIFICATE` type 2 (PKCS#7 signedData) `pkcs7` contains digest
|
||||
algorithm OIDs, embedded X.509 serial/issuer/subject/validity fields, and
|
||||
signer identifier facts. Other certificate types keep `pkcs7` null.
|
||||
|
||||
A missing table is observed absence of that directory. It is not proof the
|
||||
image is unsigned in every catalog or overlay sense.
|
||||
|
||||
## Limits and failures
|
||||
|
||||
The selected file is limited to 64 MiB by default (512 MiB maximum). At most
|
||||
eight `WIN_CERTIFICATE` entries are examined by default. Malformed PE headers,
|
||||
truncated certificate entries, and PKCS#7 that is not structurally signedData
|
||||
fail without partial success. A security directory that ends in fewer than
|
||||
eight header bytes, including zeros, is a truncated `WIN_CERTIFICATE` and
|
||||
fails the same way. SignedData must contain at least one `SignerInfo`, and
|
||||
each `SignerInfo` must carry a non-empty signature value. Decoded certificate
|
||||
name text shares a 64 KiB budget per PKCS#7 payload.
|
||||
|
||||
This workflow does not verify signatures, certificate chains, timestamps, or
|
||||
Authenticode digest coverage of image bytes.
|
||||
@@ -143,6 +143,7 @@ npx rea-agents@latest setup
|
||||
| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](../native-investigation.md) |
|
||||
| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](../binary-diagnostics.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| شيفرة 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) |
|
||||
|
||||
@@ -143,6 +143,7 @@ REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz
|
||||
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.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) |
|
||||
|
||||
@@ -149,6 +149,7 @@ npx rea-agents@latest setup
|
||||
| Δομή ELF χωρίς εκτέλεση | Ενότητες, τμήματα, αρχικά σύμβολα/εγγραφές μετεγκατάστασης και ενδείξεις πιθανών μέτρων προστασίας από στατική ανάλυση | pwntools που παρέχει ο χρήστης σε Linux x64· [διαγνωστικά δυαδικών αρχείων](../binary-diagnostics.md) |
|
||||
| Δυαδικά αρχεία Go | Ενσωματωμένη έκδοση μεταγλωττιστή, modules, αντικαταστάσεις και ρυθμίσεις μεταγλώττισης | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· δεν απαιτείται εγκατάσταση του Go |
|
||||
| Δυαδικά αρχεία Rust | Ενσωματωμένα ονόματα, εκδόσεις, πηγές και σχέσεις εξάρτησης των crates από το cargo-auditable | Ενσωματωμένη στατική εξέταση ELF/PE/thin Mach-O· [δυαδικά αρχεία Rust](../rust-binaries.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| Αρχείο WASM | Έλεγχο εγκυρότητας, ενότητες, ακριβείς μορφές εισαγωγών/εξαγωγών σε WAT και αποκωδικοποιημένο WAT | WABT 1.0.42 που παρέχει ο χρήστης· `inspect_wasm_artifact` |
|
||||
| Bytecode EVM | Selectors επιλογής συνάρτησης, μετατοπίσεις σε bytes, εκτιμώμενα ορίσματα και δυνατότητα μεταβολής της κατάστασης | Τοπική είσοδος σε ακατέργαστα bytes ή δεκαεξαδική μορφή σε Linux x64· [οδηγός EVM χωρίς εκτέλεση](../evm-bytecode.md) |
|
||||
| Βάσεις δεδομένων SQLite | Πίνακες, στήλες, ευρετήρια, όψεις, triggers και επιλεγμένες γραμμές κανονικών πινάκων | Τοπικό στιγμιότυπο βάσης δεδομένων/WAL· Node.js 24.x >=24.15 ή 26+ με εγγενή όρια SQLite |
|
||||
|
||||
@@ -143,6 +143,7 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h
|
||||
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.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) |
|
||||
|
||||
@@ -135,6 +135,7 @@ REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، بههمراه np
|
||||
| ---------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| باینریهای بومی | شبهکد، اسمبلی، رشتهها، نمادها، فراخوانیها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](../native-investigation.md) |
|
||||
| ساختار ELF آفلاین | بخشها، سگمنتها، نمادها و relocationهای اصلی و گزینههای احتمالی کاهش آسیبپذیری ایستا | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](../binary-diagnostics.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| بایتکد 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) |
|
||||
|
||||
@@ -146,6 +146,7 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.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) |
|
||||
|
||||
@@ -149,6 +149,7 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 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) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.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) |
|
||||
|
||||
@@ -146,6 +146,7 @@ REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strument
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.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) |
|
||||
|
||||
@@ -143,6 +143,7 @@ REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](../native-investigation.md) |
|
||||
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](../binary-diagnostics.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| 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) |
|
||||
|
||||
@@ -153,6 +153,7 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
|
||||
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](../binary-diagnostics.md) |
|
||||
| Go 바이너리 | 내장된 컴파일러 버전, 모듈, `replace` 지시어, 빌드 설정 | REA 내장 ELF/PE/thin Mach-O 정적 검사; Go 설치 불필요 |
|
||||
| Rust 바이너리 | cargo-auditable이 내장한 크레이트 이름, 버전, 소스, 의존성 관계 | REA 내장 ELF/PE/thin Mach-O 정적 검사; [Rust 바이너리](../rust-binaries.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| WASM 아티팩트 | 유효성 검사, 섹션, import/export의 정확한 WAT 형식, 디코딩된 WAT | 호출자 제공 WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
|
||||
| SQLite 데이터베이스 | 테이블, 열, 인덱스, 뷰, 트리거, 선택한 일반 테이블의 행 | 로컬 데이터베이스/WAL 스냅샷; Node.js 24.15 이상의 24.x 또는 26 이상(네이티브 SQLite 한도 적용) |
|
||||
|
||||
@@ -151,6 +151,7 @@ REA-യ്ക്ക് Node.js 22.x (>=22.19), 24.x (>=24.11), അല്ലെ
|
||||
| Offline ELF layout | Sections, segments, യഥാർത്ഥ symbols/relocations, static mitigation candidates | Linux x64-ൽ caller നൽകുന്ന pwntools; [binary diagnostics](../binary-diagnostics.md) |
|
||||
| Go binaries | ഉൾച്ചേർത്ത compiler version, modules, replacements, build settings | Built-in static ELF/PE/thin Mach-O പരിശോധന; Go installation ആവശ്യമില്ല |
|
||||
| Rust binaries | ഉൾച്ചേർത്ത cargo-auditable crate പേരുകൾ, versions, sources, dependency edges | Built-in static ELF/PE/thin Mach-O പരിശോധന; [Rust binaries](../rust-binaries.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| WASM artifact | Validation, sections, കൃത്യമായ import/export WAT രൂപങ്ങൾ, decoded WAT | Caller നൽകുന്ന WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM bytecode | Dispatch selectors, byte offsets, അനുമാനിച്ച arguments, mutability | Linux x64-ൽ local raw/hex carrier; [offline EVM guide](../evm-bytecode.md) |
|
||||
| SQLite databases | Tables, columns, indexes, views, triggers, തിരഞ്ഞെടുത്ത സാധാരണ table rows | Local database/WAL snapshot; native SQLite പരിമിതികളോടെ Node.js 24.x >=24.15 അല്ലെങ്കിൽ 26+ |
|
||||
|
||||
@@ -143,6 +143,7 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na
|
||||
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| 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) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.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) |
|
||||
|
||||
@@ -143,6 +143,7 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer
|
||||
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 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) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.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) |
|
||||
|
||||
@@ -145,6 +145,7 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
|
||||
| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](../binary-diagnostics.md) |
|
||||
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
|
||||
| Установленные настольные приложения | Возможные среды выполнения, файлы с доказательствами, компоненты и дальнейшие шаги | Ограниченная проверка каталогов и заголовков;[Настольные приложения](../desktop-applications.md) |
|
||||
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](../javascript-application-workflows.md) |
|
||||
|
||||
@@ -143,6 +143,7 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26
|
||||
| ---------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](../native-investigation.md) |
|
||||
| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](../binary-diagnostics.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| ไบต์โค้ด 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) |
|
||||
|
||||
@@ -147,6 +147,7 @@ REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araç
|
||||
| Çevrimdışı ELF yapısı | Bölümler, segmentler, özgün semboller/yeniden konumlandırmalar ve statik analizden olası güvenlik önlemleri | Linux x64 üzerinde çağıranın sağladığı pwntools; [ikili dosya tanılama](../binary-diagnostics.md) |
|
||||
| Go ikili dosyaları | Gömülü compiler sürümü, modüller, replacement'lar ve build ayarları | Yerleşik statik ELF/PE/thin Mach-O incelemesi; Go kurulumu gerekmez |
|
||||
| Rust ikili dosyaları | Gömülü cargo-auditable crate adları, sürümleri, kaynakları ve bağımlılık ilişkileri | Yerleşik statik ELF/PE/thin Mach-O incelemesi; [Rust ikili dosyaları](../rust-binaries.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| WASM artefaktı | Doğrulama, bölümler, tam import/export WAT biçimleri ve decode edilmiş WAT | Çağıranın sağladığı WABT 1.0.42; `inspect_wasm_artifact` |
|
||||
| EVM bayt kodu | Yönlendirme seçicileri, bayt ofsetleri, çıkarımla belirlenen argümanlar ve durum değiştirilebilirliği | Linux x64 üzerinde yerel ham bayt/onaltılık girdi; [çevrimdışı EVM rehberi](../evm-bytecode.md) |
|
||||
| SQLite veritabanları | Tablolar, sütunlar, index'ler, view'lar, trigger'lar ve seçilen sıradan tablo satırları | Yerel database/WAL snapshot'ı; native SQLite sınırlarıyla Node.js 24.x >=24.15 veya 26+ |
|
||||
|
||||
@@ -143,6 +143,7 @@ REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а та
|
||||
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](../native-investigation.md) |
|
||||
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](../binary-diagnostics.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| Байт-код 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) |
|
||||
|
||||
@@ -145,6 +145,7 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
|
||||
| Bố cục ELF ngoại tuyến | Các section, segment, ký hiệu/thông tin tái định vị gốc và các biện pháp giảm thiểu có thể có từ phân tích tĩnh | pwntools do bên gọi cung cấp trên Linux x64; [chẩn đoán tệp nhị phân](../binary-diagnostics.md) |
|
||||
| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân trên Linux x64; [hướng dẫn EVM ngoại tuyến](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](../recorded-crashes.md) |
|
||||
| Thư mục cài đặt ứng dụng máy tính | Các runtime có khả năng được dùng, tệp bằng chứng, dấu hiệu thành phần và bước tiếp theo | Kiểm tra thư mục và phần đầu tệp có giới hạn;[Ứng dụng máy tính](../desktop-applications.md) |
|
||||
| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](../javascript-application-workflows.md) |
|
||||
|
||||
+27
-26
@@ -141,32 +141,33 @@ AI 代理通過 MCP 呼叫 REA,檢查目標並追蹤相關程式碼。REA 返
|
||||
|
||||
REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。額外工具和主機支援取決於分析目標:
|
||||
|
||||
| 目標 | REA 返回的內容 | 要求與指南 |
|
||||
| --------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
|
||||
| 離線 ELF 佈局 | 節、段、原始符號/重定位資訊及靜態防護機制候選項 | Linux x64 上由呼叫方提供的 pwntools;[二進位診斷](../binary-diagnostics.md) |
|
||||
| Go 二進位檔案 | 內嵌編譯器版本、模組、替換項和建置設定 | 內建靜態 ELF/PE/thin Mach-O 檢查;無需安裝 Go |
|
||||
| WASM 工件 | 驗證、節、精確的匯入/匯出 WAT 形式及解碼後的 WAT | 呼叫方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
|
||||
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) |
|
||||
| 桌面安裝目錄 | 執行階段候選、支持檔案、元件標記與後續操作 | 有界的目錄與檔頭檢查;[桌面應用](../desktop-applications.md) |
|
||||
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](../javascript-application-workflows.md) |
|
||||
| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](../browser-observation.md) |
|
||||
| 已保存的網路擷取 | 請求、回應、可訪問的酬載和來源位置 | HAR;原生 mitmproxy 擷取需要 Linux 上的 mitmdump;[擷取指南](../web-network-captures.md) |
|
||||
| 封包擷取 | 端點、DNS、TLS SNI、通訊協定分佈和 HTTP 請求 | 呼叫端提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
|
||||
| .NET 程式集 | 中繼資料、CIL 指令、聲明的原生依賴和建置對比 | 靜態檢查;[托管程式碼指南](../managed-code-analysis.md) |
|
||||
| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
|
||||
| Android APK | 清單聲明、類別、反編譯的方法和引用 | Linux/macOS/Windows x64 上的無界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
|
||||
| Android 裝置 | 裝置、套件、程序、日誌、傾印、螢幕擷取以及 APK/檔案傳輸 | 呼叫端提供的 adb(模擬器或實體裝置);[ADB 指南](../adb-device-analysis.md) |
|
||||
| JEB 專案 | 引擎支援的專案單元、成品摘要和型別/方法的虛擬碼 | 由呼叫端啟動、提供 MCP 的 JEB 用戶端,透過 REA_JEB_MCP_URL 選擇;[JEB 指南](../jeb-analysis.md) |
|
||||
| Android 資源 | 解碼後的清單、字串表、語系和版本資訊 | 呼叫端提供的 Apktool 啟動器;[資源指南](../apktool-resource-analysis.md) |
|
||||
| Flutter 應用 | Dart 快照雜湊、依 ABI 的程式庫雜湊及引擎建置來源 | 無(靜態 APK 解析);[Flutter 指南](../flutter-analysis.md) |
|
||||
| 韌體 | 區域、提取結果和轉交原生分析的內容 | Linux 上的 Binwalk / Unblob;[韌體指南](../firmware-analysis.md) |
|
||||
| 封包記錄 | 指定欄位、重複值、篩選條件與解碼規則 | Linux: TShark 4.x + prlimit; [封包分析指南](../packet-captures.md) |
|
||||
| 錄製的邏輯訊號 | 解碼註解、樣本範圍與診斷資訊 | Linux: sigrok-cli 0.7.2 + prlimit; [訊號記錄指南](../signal-recordings.md) |
|
||||
| 軟體包與資源 | 檔案清單、摘要、plist、Apple bundle 結構和提取的資源 | [製品與 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 應用](../apple-application-analysis.md) |
|
||||
| 處理程序行為 | 終端輸出、互動、退出和檔案系統觀察結果,以及執行對比 | 支援原生 PTY 的 Linux/macOS;[處理程序擷取](../process-capture.md) |
|
||||
| 目標 | REA 返回的內容 | 要求與指南 |
|
||||
| --------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
|
||||
| 離線 ELF 佈局 | 節、段、原始符號/重定位資訊及靜態防護機制候選項 | Linux x64 上由呼叫方提供的 pwntools;[二進位診斷](../binary-diagnostics.md) |
|
||||
| Go 二進位檔案 | 內嵌編譯器版本、模組、替換項和建置設定 | 內建靜態 ELF/PE/thin Mach-O 檢查;無需安裝 Go |
|
||||
| WASM 工件 | 驗證、節、精確的匯入/匯出 WAT 形式及解碼後的 WAT | 呼叫方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
|
||||
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) |
|
||||
| 桌面安裝目錄 | 執行階段候選、支持檔案、元件標記與後續操作 | 有界的目錄與檔頭檢查;[桌面應用](../desktop-applications.md) |
|
||||
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](../javascript-application-workflows.md) |
|
||||
| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](../browser-observation.md) |
|
||||
| 已保存的網路擷取 | 請求、回應、可訪問的酬載和來源位置 | HAR;原生 mitmproxy 擷取需要 Linux 上的 mitmdump;[擷取指南](../web-network-captures.md) |
|
||||
| 封包擷取 | 端點、DNS、TLS SNI、通訊協定分佈和 HTTP 請求 | 呼叫端提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
|
||||
| .NET 程式集 | 中繼資料、CIL 指令、聲明的原生依賴和建置對比 | 靜態檢查;[托管程式碼指南](../managed-code-analysis.md) |
|
||||
| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
|
||||
| Android APK | 清單聲明、類別、反編譯的方法和引用 | Linux/macOS/Windows x64 上的無界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
|
||||
| Android 裝置 | 裝置、套件、程序、日誌、傾印、螢幕擷取以及 APK/檔案傳輸 | 呼叫端提供的 adb(模擬器或實體裝置);[ADB 指南](../adb-device-analysis.md) |
|
||||
| JEB 專案 | 引擎支援的專案單元、成品摘要和型別/方法的虛擬碼 | 由呼叫端啟動、提供 MCP 的 JEB 用戶端,透過 REA_JEB_MCP_URL 選擇;[JEB 指南](../jeb-analysis.md) |
|
||||
| Android 資源 | 解碼後的清單、字串表、語系和版本資訊 | 呼叫端提供的 Apktool 啟動器;[資源指南](../apktool-resource-analysis.md) |
|
||||
| Flutter 應用 | Dart 快照雜湊、依 ABI 的程式庫雜湊及引擎建置來源 | 無(靜態 APK 解析);[Flutter 指南](../flutter-analysis.md) |
|
||||
| 韌體 | 區域、提取結果和轉交原生分析的內容 | Linux 上的 Binwalk / Unblob;[韌體指南](../firmware-analysis.md) |
|
||||
| 封包記錄 | 指定欄位、重複值、篩選條件與解碼規則 | Linux: TShark 4.x + prlimit; [封包分析指南](../packet-captures.md) |
|
||||
| 錄製的邏輯訊號 | 解碼註解、樣本範圍與診斷資訊 | Linux: sigrok-cli 0.7.2 + prlimit; [訊號記錄指南](../signal-recordings.md) |
|
||||
| 軟體包與資源 | 檔案清單、摘要、plist、Apple bundle 結構和提取的資源 | [製品與 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 應用](../apple-application-analysis.md) |
|
||||
| 處理程序行為 | 終端輸出、互動、退出和檔案系統觀察結果,以及執行對比 | 支援原生 PTY 的 Linux/macOS;[處理程序擷取](../process-capture.md) |
|
||||
|
||||
靜態 JavaScript 和 .NET 檢查讀取提供的檔案,不執行應用。執行階段擷取會以你的使用者權限執行目標或與之互動;各執行階段指南說明瞭具體影響。
|
||||
|
||||
|
||||
+27
-26
@@ -141,32 +141,33 @@ npx rea-agents@latest setup
|
||||
|
||||
REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。额外工具和主机支持取决于分析目标:
|
||||
|
||||
| 目标 | REA 返回的内容 | 要求与指南 |
|
||||
| --------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
|
||||
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](../binary-diagnostics.md) |
|
||||
| Go 二进制文件 | 内嵌编译器版本、模块、替换项和构建设置 | 内置静态 ELF/PE/thin Mach-O 检查;无需安装 Go |
|
||||
| WASM 工件 | 验证、节、精确的导入/导出 WAT 形式及解码后的 WAT | 调用方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
|
||||
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
|
||||
| 桌面安装目录 | 运行时候选、支持文件、组件标记和后续操作 | 有界的目录与文件头检查;[桌面应用](../desktop-applications.md) |
|
||||
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](../javascript-application-workflows.md) |
|
||||
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](../browser-observation.md) |
|
||||
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](../web-network-captures.md) |
|
||||
| 数据包捕获 | 端点、DNS、TLS SNI、协议分布和 HTTP 请求 | 调用方提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
|
||||
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](../managed-code-analysis.md) |
|
||||
| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
|
||||
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS/Windows x64 上的无界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
|
||||
| Android 设备 | 设备、软件包、进程、日志、转储、屏幕截图以及 APK/文件传输 | 调用方提供的 adb(模拟器或实体设备);[ADB 指南](../adb-device-analysis.md) |
|
||||
| JEB 项目 | 引擎支持的项目单元、工件摘要和类型/方法的伪代码 | 由调用方启动、提供 MCP 的 JEB 客户端,通过 REA_JEB_MCP_URL 选择;[JEB 指南](../jeb-analysis.md) |
|
||||
| Android 资源 | 解码后的清单、字符串表、语言区域和版本信息 | 调用方提供的 Apktool 启动器;[资源指南](../apktool-resource-analysis.md) |
|
||||
| Flutter 应用 | Dart 快照哈希、按 ABI 的库摘要及引擎构建溯源 | 无(静态 APK 解析);[Flutter 指南](../flutter-analysis.md) |
|
||||
| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](../firmware-analysis.md) |
|
||||
| 抓包文件 | 指定字段、重复值、过滤条件与解码规则 | Linux: TShark 4.x + prlimit; [抓包分析指南](../packet-captures.md) |
|
||||
| 录制的逻辑信号 | 解码注释、样本范围和诊断信息 | Linux: sigrok-cli 0.7.2 + prlimit; [信号记录指南](../signal-recordings.md) |
|
||||
| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 应用](../apple-application-analysis.md) |
|
||||
| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](../process-capture.md) |
|
||||
| 目标 | REA 返回的内容 | 要求与指南 |
|
||||
| --------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
|
||||
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](../binary-diagnostics.md) |
|
||||
| Go 二进制文件 | 内嵌编译器版本、模块、替换项和构建设置 | 内置静态 ELF/PE/thin Mach-O 检查;无需安装 Go |
|
||||
| WASM 工件 | 验证、节、精确的导入/导出 WAT 形式及解码后的 WAT | 调用方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
|
||||
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| PE certificates | Authenticode WIN_CERTIFICATE entries and PKCS#7 signer/X.509 identity fields | Built-in static PE inspection; [PE certificates](../pe-certificates.md) |
|
||||
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
|
||||
| 桌面安装目录 | 运行时候选、支持文件、组件标记和后续操作 | 有界的目录与文件头检查;[桌面应用](../desktop-applications.md) |
|
||||
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](../javascript-application-workflows.md) |
|
||||
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](../browser-observation.md) |
|
||||
| 已保存的网络捕获 | 请求、响应、可访问的载荷和来源位置 | HAR;原生 mitmproxy 捕获需要 Linux 上的 mitmdump;[捕获指南](../web-network-captures.md) |
|
||||
| 数据包捕获 | 端点、DNS、TLS SNI、协议分布和 HTTP 请求 | 调用方提供的 tshark 和 capinfos;[pcap 指南](../pcap-analysis.md) |
|
||||
| .NET 程序集 | 元数据、CIL 指令、声明的原生依赖和构建对比 | 静态检查;[托管代码指南](../managed-code-analysis.md) |
|
||||
| PyInstaller | CArchive, PYZ, Python bytecode | Static inspection; [Python packaged applications](../python-packaged-applications.md) |
|
||||
| Android APK | 清单声明、类、反编译的方法和引用 | Linux/macOS/Windows x64 上的无界面 JADX 和完整 JDK;[Android 指南](../android-analysis.md) |
|
||||
| Android 设备 | 设备、软件包、进程、日志、转储、屏幕截图以及 APK/文件传输 | 调用方提供的 adb(模拟器或实体设备);[ADB 指南](../adb-device-analysis.md) |
|
||||
| JEB 项目 | 引擎支持的项目单元、工件摘要和类型/方法的伪代码 | 由调用方启动、提供 MCP 的 JEB 客户端,通过 REA_JEB_MCP_URL 选择;[JEB 指南](../jeb-analysis.md) |
|
||||
| Android 资源 | 解码后的清单、字符串表、语言区域和版本信息 | 调用方提供的 Apktool 启动器;[资源指南](../apktool-resource-analysis.md) |
|
||||
| Flutter 应用 | Dart 快照哈希、按 ABI 的库摘要及引擎构建溯源 | 无(静态 APK 解析);[Flutter 指南](../flutter-analysis.md) |
|
||||
| 固件 | 区域、提取结果和转交原生分析的内容 | Linux 上的 Binwalk / Unblob;[固件指南](../firmware-analysis.md) |
|
||||
| 抓包文件 | 指定字段、重复值、过滤条件与解码规则 | Linux: TShark 4.x + prlimit; [抓包分析指南](../packet-captures.md) |
|
||||
| 录制的逻辑信号 | 解码注释、样本范围和诊断信息 | Linux: sigrok-cli 0.7.2 + prlimit; [信号记录指南](../signal-recordings.md) |
|
||||
| 软件包与资源 | 文件清单、摘要、plist、Apple bundle 结构和提取的资源 | [制品与 JavaScript 指南](../javascript-artifact-reconstruction.md)、[Apple 应用](../apple-application-analysis.md) |
|
||||
| 进程行为 | 终端输出、交互、退出和文件系统观察结果,以及运行对比 | 支持原生 PTY 的 Linux/macOS;[进程捕获](../process-capture.md) |
|
||||
|
||||
静态 JavaScript 和 .NET 检查读取提供的文件,不运行应用。运行时捕获会以你的用户权限运行目标或与之交互;各运行时指南说明了具体影响。
|
||||
|
||||
|
||||
+12
-3
@@ -86,7 +86,7 @@ The inline Evidence result contains the artifact path, SHA-256, byte length,
|
||||
container format, architecture, word size, and byte order. When a `.dep-v0`
|
||||
section is present, `auditable_metadata` contains:
|
||||
|
||||
- `section_name`, `.dep-v0` for ELF and PE or `__DATA,.dep-v0` for thin Mach-O;
|
||||
- `section_name`, `.dep-v0` for ELF and PE or `__DATA,.dep-v0` for Mach-O;
|
||||
- `file_offset` and `section_bytes`, the exact file range of the compressed section;
|
||||
- `compressed_bytes` and `uncompressed_bytes`;
|
||||
- `format_version`, the embedded integer, or `null` when the field is omitted;
|
||||
@@ -124,8 +124,17 @@ verified. An omitted revision remains `null` and follows revision 0 semantics.
|
||||
Embedded names and versions are file content. They do not authenticate the
|
||||
packages or the compiler.
|
||||
|
||||
Universal Mach-O containers are unsupported. Select a thin slice first. ELF,
|
||||
PE, and thin Mach-O are the supported containers.
|
||||
ELF, PE, thin Mach-O and one selected slice of a universal Mach-O are the
|
||||
supported containers.
|
||||
A universal (FAT) Mach-O needs `architecture` (`x86`, `x86_64`, `arm`
|
||||
or `arm64`; CLI `--architecture`). REA reads its own FAT table, without `lipo`,
|
||||
and validates the selected slice's range, alignment and Mach-O header CPU
|
||||
against the table. The result keeps the universal file as `artifact` (its
|
||||
SHA-256 and size), adds `selected_slice` with the slice offset, length, raw CPU
|
||||
type/subtype and the FAT table digest, and reports the section and rustc-path offsets as a
|
||||
file offset inside that slice. Without `architecture`, or when the file has no
|
||||
such slice or several, the request fails on `architecture` and lists the slices
|
||||
present. `architecture` on any other image is rejected rather than ignored.
|
||||
|
||||
Package and dependency counts are checked against the minimum required JSON
|
||||
representation before graph allocation or schema cloning. Oversized graphs
|
||||
|
||||
@@ -7,6 +7,7 @@ import { parseArgs } from "node:util";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { sampleRetainedHeap } from "./mcp-retained-heap.mjs";
|
||||
import {
|
||||
captureRuntimeBinding,
|
||||
assertRuntimeBinding,
|
||||
@@ -24,10 +25,12 @@ const { values } = parseArgs({
|
||||
"cache-root": { type: "string" },
|
||||
bindings: { type: "string" },
|
||||
role: { type: "string" },
|
||||
"diagnostic-heap": { type: "boolean", default: false },
|
||||
},
|
||||
});
|
||||
assert.ok(["analysis", "discovery"].includes(values.phase));
|
||||
assert.ok(["cold", "warm"].includes(values.cache));
|
||||
assert.ok(!values["diagnostic-heap"] || values.phase === "analysis");
|
||||
for (const key of values.phase === "analysis"
|
||||
? ["runtime", "artifact", "oracle", "output"]
|
||||
: ["runtime", "output"])
|
||||
@@ -60,7 +63,11 @@ if (values.bindings !== undefined) {
|
||||
const inputBytes = await readFile(values.bindings);
|
||||
measurementInputsSha256 = hash(inputBytes);
|
||||
const inputs = JSON.parse(inputBytes.toString("utf8"));
|
||||
for (const name of ["mcp-input-catalog.mjs", "managed-cli-runtime.mjs"]) {
|
||||
for (const name of [
|
||||
"mcp-input-catalog.mjs",
|
||||
"managed-cli-runtime.mjs",
|
||||
"mcp-retained-heap.mjs",
|
||||
]) {
|
||||
producerSources[name] = hash(
|
||||
await readFile(new URL(name, import.meta.url)),
|
||||
);
|
||||
@@ -87,7 +94,11 @@ const environment = {
|
||||
};
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [join(values.runtime, "scripts/rea.mjs"), "mcp"],
|
||||
args: [
|
||||
...(values["diagnostic-heap"] ? ["--inspect=127.0.0.1:0"] : []),
|
||||
join(values.runtime, "scripts/rea.mjs"),
|
||||
"mcp",
|
||||
],
|
||||
cwd: values.output,
|
||||
env: environment,
|
||||
stderr: "pipe",
|
||||
@@ -118,8 +129,13 @@ const report = {
|
||||
};
|
||||
const transcript = [];
|
||||
let stderr = "";
|
||||
let inspectorEndpoint;
|
||||
transport.stderr?.on("data", (value) => {
|
||||
stderr += value;
|
||||
if (values["diagnostic-heap"] && inspectorEndpoint === undefined)
|
||||
inspectorEndpoint = stderr.match(
|
||||
/Debugger listening on (ws:\/\/127\.0\.0\.1:\d+\/[^\s]+)/u,
|
||||
)?.[1];
|
||||
});
|
||||
const began = performance.now();
|
||||
try {
|
||||
@@ -255,6 +271,15 @@ try {
|
||||
);
|
||||
report.status = "passed";
|
||||
}
|
||||
if (values["diagnostic-heap"]) {
|
||||
report.status = "incomplete";
|
||||
assert.ok(inspectorEndpoint, "Missing explicitly launched inspector");
|
||||
report.diagnostic_heap = await sampleRetainedHeap(
|
||||
inspectorEndpoint,
|
||||
transport.pid,
|
||||
);
|
||||
report.status = "passed";
|
||||
}
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,143 @@
|
||||
# Integer validator allocation after MCP materialization
|
||||
|
||||
Issue #6236; PR #4360. Linux x64, 2026-10-22.
|
||||
|
||||
<!-- BEGIN:allocation-report:provenance -->
|
||||
|
||||
Baseline `main@4a4bbab3c337701febeb6b532ace49041c4876e0`; candidate `461f342c71a72ef2be60912391df2393aa4e2ea3`. This is a pinned comparison.
|
||||
|
||||
In separate inspector/forced-GC diagnostics, retained REA V8 heap decreases by about 9.6 MiB. The three-pair ranges do not overlap in either profile. Inspector perturbation is included. Normal task timing, CPU and peak memory are reported separately; these observations do not establish a general latency or peak-memory reduction.
|
||||
|
||||
<!-- END:allocation-report:provenance -->
|
||||
|
||||
Directory discovery uses recorded advertisements after #4424; this study separates it from completed tasks that materialize canonical handlers. Context is unchanged.
|
||||
|
||||
## Change and compatibility
|
||||
|
||||
Replace 1,247 argument-free `z.number().int()` constructions with `z.int()` across 175 existing source files. Keep current shared schema owners, refinements, bounds, defaults and metadata. AST-token comparison against the baseline, after normalizing this construction and formatting, is equal for every touched source file. No new tools, provider effects, caches or response contracts.
|
||||
|
||||
The locked Zod 4.4.3 chained form retains a `ZodNumber` with a separate `ZodNumberFormat` check; direct construction uses the format itself. Incur 0.6.1 coerces argv strings only for `ZodNumber`. Existing CLI declarations and shared-input exceptions therefore retain the chained form. The actual compiled CLI registers 160 leaf commands; 104 numeric argument/option fields have identical results/errors in 974 installed-Incur parser cases across both runtimes, each field with an accepted probe. Fields are isolated from unrelated required options and no handlers are invoked. This is consumer-boundary coverage, not a real-engine claim.
|
||||
|
||||
Seven dependency-boundary variants match parsed values and complete error issues for 126 cases, including fractions, nonfinite values, strings/null/undefined and unsafe integers; 14 JSON Schema projections match in draft-07/draft-2020-12. Actual SDK advertisements match byte-for-byte in both profiles before/after materialization and across roles.
|
||||
|
||||
## Tasks, artifacts and bounds
|
||||
|
||||
Reuse the shared SDK task driver and independent ELF/section/zlib/JSON oracles from [the catalog study](mcp-input-catalog.md): official Alpine v3.21 x86_64 ripgrep 14.1.1-r0 and Zellij 0.40.0-r0. All 42/422 packages and 88/1,063 edges match the independent oracles. Select serde name/version/kind, repeat exact retained-parent reads twice, verify a canonical negative-offset refusal, export/check complete Evidence and confirm the unchanged directory. `kind: null` stays unknown. Targets are not executed; dependency presence does not establish runtime use. Acquisition URLs, archive/executable/oracle hashes and complete answers are in [the receipts](mcp-integer-allocation.json). Previously acquired public bytes are rehashed, not redownloaded.
|
||||
|
||||
Normal cohort: three alternating baseline/candidate pairs for each task × cold/warm server compile cache × default compact/full inputs: 72 trials, plus four excluded full-task warm priming runs. Separate diagnostic cohort: three alternating pairs per profile, cold Zellij tasks, 12 trials. All pass, with identical complete parent IDs/hashes and answers.
|
||||
|
||||
Node 24.18.0/npm 11.16.0; fresh client/server and HOME per trial, driver compile cache disabled. Each trial is serial, pinned to one CPU, 4 GiB hard/3 GiB early stop, no swap, at most 192 tasks. Before/after checks bind clean source, all compiled JS/JSON assets (1,990 baseline / 1,990 candidate files), scripts and manifests; producer and resource-observer hashes are also bound. No OOM, resource stop or leftover payload processes. Whole-family measurements include SDK validation, extra calls, source/compiled audits and oracle checking; kernel peak includes file cache. Build/acquisition/oracle generation and priming are excluded. Warm uses a populated role/profile-specific Node cache; hit rate and OS page cache are unmeasured, and audits warm compiled-file pages.
|
||||
|
||||
## Normal task costs
|
||||
|
||||
Medians, baseline → candidate; all ranges are in the JSON. No inspector or diagnostic GC runs in this cohort.
|
||||
|
||||
<!-- BEGIN:allocation-report:normal -->
|
||||
|
||||
| Task | Cache | Input profile | Wall s | Family CPU s | Family peak MiB |
|
||||
| --------- | ----- | ------------- | --------------: | --------------: | --------------: |
|
||||
| directory | cold | default | 2.739 → 2.239 | 2.553 → 2.075 | 247.4 → 248.4 |
|
||||
| directory | cold | full | 2.940 → 2.437 | 2.682 → 2.217 | 348.0 → 346.8 |
|
||||
| directory | warm | default | 2.639 → 2.135 | 2.461 → 1.980 | 251.8 → 246.2 |
|
||||
| directory | warm | full | 2.840 → 2.338 | 2.666 → 2.144 | 348.6 → 347.2 |
|
||||
| ripgrep | cold | default | 7.571 → 7.169 | 7.372 → 6.918 | 798.8 → 791.1 |
|
||||
| zellij | cold | default | 7.876 → 7.373 | 7.623 → 7.134 | 812.2 → 810.5 |
|
||||
| ripgrep | cold | full | 7.876 → 7.475 | 7.710 → 7.295 | 803.7 → 801.2 |
|
||||
| zellij | cold | full | 8.079 → 7.573 | 7.852 → 7.378 | 815.7 → 819.1 |
|
||||
| ripgrep | warm | default | 7.575 → 6.977 | 7.352 → 6.810 | 815.0 → 791.6 |
|
||||
| zellij | warm | default | 7.774 → 7.178 | 7.581 → 6.981 | 820.9 → 822.0 |
|
||||
| ripgrep | warm | full | 7.976 → 7.875 | 7.746 → 7.679 | 812.7 → 789.3 |
|
||||
| zellij | warm | full | 14.995 → 13.978 | 14.742 → 13.718 | 818.5 → 811.5 |
|
||||
|
||||
<!-- END:allocation-report:normal -->
|
||||
|
||||
### SDK span and audit separation
|
||||
|
||||
<!-- BEGIN:allocation-report:sdk -->
|
||||
|
||||
Cold/default directory audit wall medians are 1.273 s baseline / 0.782 s candidate; SDK spans are 0.944 → 0.948 s. Whole-family costs include those experimental audits. Their cause and per-span CPU are unmeasured.
|
||||
|
||||
SDK span from client connect through task checks and client/server close. Excludes pre/post runtime binding and preflight artifact/oracle loading. Diagnostic spans include inspector and forced GC. CPU is measured for the whole family only; per-span CPU is unmeasured.
|
||||
|
||||
| Task | Cache | Inputs | SDK span s, baseline → candidate |
|
||||
| --------- | ----- | ------- | -------------------------------: |
|
||||
| directory | cold | default | 0.944 → 0.948 |
|
||||
| directory | cold | full | 1.067 → 1.102 |
|
||||
| directory | warm | default | 0.888 → 0.878 |
|
||||
| directory | warm | full | 1.038 → 1.042 |
|
||||
| ripgrep | cold | default | 5.778 → 5.752 |
|
||||
| zellij | cold | default | 5.998 → 5.991 |
|
||||
| ripgrep | cold | full | 6.077 → 6.119 |
|
||||
| zellij | cold | full | 6.228 → 6.209 |
|
||||
| ripgrep | warm | default | 5.743 → 5.656 |
|
||||
| zellij | warm | default | 5.965 → 5.788 |
|
||||
| ripgrep | warm | full | 6.100 → 6.492 |
|
||||
| zellij | warm | full | 11.776 → 11.529 |
|
||||
|
||||
<!-- END:allocation-report:sdk -->
|
||||
|
||||
Full-profile warm Zellij is slower than its cold condition in both roles in this cohort. Cache hit rate and the cause of this observation are unmeasured, and cold/warm groups run sequentially. These timings support neither a general cache speedup nor a causal regression attribution.
|
||||
|
||||
## Separate retained-heap diagnostics
|
||||
|
||||
The same completed SDK task launches only its owned REA process with `--inspect=127.0.0.1:0`. Four CDP calls verify the owned transport PID, read heap, force GC and read heap again. The endpoint comes from that child’s stderr; the PID is a cross-check of the owned endpoint, not sole ownership proof. The inspector socket closes before MCP shutdown. These V8 values include inspector perturbation and exclude the client; diagnostic wall/CPU/family peaks include inspector and forced-GC overhead and are not normal-task timing results.
|
||||
|
||||
<!-- BEGIN:allocation-report:heap -->
|
||||
|
||||
| Inputs | REA V8 retained MiB | Baseline range | Candidate range | Diagnostic wall s | CPU s | Family peak MiB |
|
||||
| ------- | ------------------: | --------------: | --------------: | ----------------: | --------------: | --------------: |
|
||||
| default | 189.835 → 180.242 | 189.823–189.855 | 180.208–180.366 | 15.490 → 14.291 | 15.246 → 14.123 | 814.1 → 786.3 |
|
||||
| full | 191.260 → 181.693 | 191.165–191.304 | 181.588–181.848 | 8.979 → 8.377 | 8.769 → 8.142 | 810.3 → 824.9 |
|
||||
|
||||
<!-- END:allocation-report:heap -->
|
||||
|
||||
A mismatched PID is rejected; removing only that guard makes the negative oracle fail. Both connections in that check use the same owned REA endpoint, with no foreign-process attachment. Aggregation rejects diagnostic GC in a normal trial. Historical #4424 receipt reaggregation preserves previous metrics and adds the separately reported SDK-span fields; its aggregator source hash changes.
|
||||
|
||||
## Context accounting
|
||||
|
||||
<!-- BEGIN:allocation-report:context -->
|
||||
|
||||
tiktoken==0.14.0/cl100k_base; estimates, not model usage. Actual model usage is unmeasured.
|
||||
|
||||
| Profile | Input directory once | Directory task | ripgrep task | Zellij task |
|
||||
| ------- | -------------------: | -------------: | -----------: | ----------: |
|
||||
| default | 49,670 | 50,840 | 54,029 | 53,949 |
|
||||
| full | 205,744 | 206,914 | 210,103 | 210,023 |
|
||||
|
||||
Raw SDK directory costs per list: 756,165/912,436 tokens and 2,935,148/3,587,792 bytes for default/full.
|
||||
|
||||
Question, explicit recipe, server instructions, one complete name/description/input-schema directory, every tool argument and text reply, plus prompt directory for discovery. This projection assumes the client retains a single unchanged directory. Raw catalog result costs include every observed tools/list result separately, with output schemas and without JSON-RPC envelopes. SDK initialization/framing, reasoning and actual model usage are unmeasured. The analysis task includes retained-read, rejection, export/oracle and post-materialization directory checks.
|
||||
|
||||
<!-- END:allocation-report:context -->
|
||||
|
||||
## Validation and reproduction
|
||||
|
||||
- Both runtime builds and all six static checks plus architecture guards pass. Before the final main synchronization, 5,972 source/contract/public CLI/MCP tests pass across 323 files; four skip for unavailable/guarded coverage. After synchronization, both builds/static checks repeat successfully, 77 affected producer and CLI/MCP tests pass, and all 974 registered numeric-parameter parser cases match again. Includes real SQLite CLI/MCP record/column selection and JavaScript heap/deadline preservation. A first direct schema-validity invocation lacked generated catalog metadata; the supported focused entry generates it and all 15 cases pass. A later affected-test invocation selected two nonexistent paths; the corrected three-file producer/consumer run is the 77-case result above. Neither setup failure was counted as a passing test, and no production or timeout change was needed.
|
||||
- All 84 measured tasks and four warm priming tasks pass independent oracles, canonical refusal, parent/directory identity and owned cleanup checks. This does not verify external Hopper/Ghidra/IDA, Android devices, all providers or foreign OS behavior. The earlier macOS numbers in #4360 describe a different revision/platform and are not combined with these results.
|
||||
|
||||
Build the pinned baseline/candidate in separate clean worktrees with the locked toolchain. Use the candidate’s producer scripts for both roles. `measurement_inputs` in the JSON records the complete matrix, questions/recipes, runtime bindings and producer hashes; extract it unchanged as the bindings file. Historical receipts require their matching producer revision. One normal task:
|
||||
|
||||
```bash
|
||||
NODE_DISABLE_COMPILE_CACHE=1 node experiments/analysis-efficiency/mcp-input-catalog.mjs \
|
||||
--runtime "$CANDIDATE_RUNTIME" --bindings "$MEASUREMENT_INPUTS" --role candidate \
|
||||
--artifact "$RIPGREP_BINARY" --oracle "$RIPGREP_ORACLE" \
|
||||
--output "$NEW_CASE_DIRECTORY" --profile default --phase analysis --cache cold
|
||||
```
|
||||
|
||||
Run each task through `resource-run.py --observe-payload-exit --` to obtain whole-family counters. For warm runs replace `--cache cold` with `--cache warm --cache-root <role/profile cache>` after one complete priming task. For the separate Zellij diagnostic tasks add `--diagnostic-heap` and use cold cache. Keep their case names/logs aligned with `measurement_inputs`; aggregate both cohorts with:
|
||||
|
||||
```bash
|
||||
python experiments/analysis-efficiency/mcp-startup-costs.py "$RECEIPT_ROOT" \
|
||||
--output mcp-integer-allocation.json
|
||||
```
|
||||
|
||||
The aggregator requires the recorded tiktoken version, exact catalog/parent/runtime/producer/guard identities, clean source, successful payload cleanup and matching matrix coverage. Default driver behavior continues without an inspector; the heap flag is confined to the experiment.
|
||||
|
||||
Regenerate or verify every numerical section directly from the committed JSON:
|
||||
|
||||
```bash
|
||||
node experiments/analysis-efficiency/mcp-integer-report.mjs
|
||||
node experiments/analysis-efficiency/mcp-integer-report.mjs --check
|
||||
```
|
||||
|
||||
The documentation conformance gate checks current receipts, rejects stale normal/heap tables, and refuses missing producer markers without writing.
|
||||
@@ -0,0 +1,212 @@
|
||||
// One producer for the numerical sections of the committed allocation report.
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
data: { type: "string" },
|
||||
markdown: { type: "string" },
|
||||
check: { type: "boolean", default: false },
|
||||
},
|
||||
});
|
||||
const dataPath =
|
||||
values.data ?? new URL("mcp-integer-allocation.json", import.meta.url);
|
||||
const markdownPath =
|
||||
values.markdown ?? new URL("mcp-integer-allocation.md", import.meta.url);
|
||||
const data = JSON.parse(await readFile(dataPath, "utf8"));
|
||||
assert.equal(data.status, "passed");
|
||||
const original = await readFile(markdownPath, "utf8");
|
||||
const fixed = (value, digits = 3) => {
|
||||
assert.ok(Number.isFinite(value));
|
||||
return value.toFixed(digits);
|
||||
};
|
||||
const mib = (value, digits = 1) => fixed(value / 2 ** 20, digits);
|
||||
const median = (values) => {
|
||||
assert.ok(values.length > 0);
|
||||
const sorted = [...values].sort((a, b) => a - b);
|
||||
const middle = Math.floor(sorted.length / 2);
|
||||
return sorted.length % 2
|
||||
? sorted[middle]
|
||||
: (sorted[middle - 1] + sorted[middle]) / 2;
|
||||
};
|
||||
const table = (headers, rows, right = []) =>
|
||||
[
|
||||
`| ${headers.join(" | ")} |`,
|
||||
`| ${headers.map((_, index) => (right.includes(index) ? "---:" : "---")).join(" | ")} |`,
|
||||
...rows.map((row) => `| ${row.join(" | ")} |`),
|
||||
].join("\n");
|
||||
const pair = (item, key, format = fixed) =>
|
||||
`${format(item.baseline[key].median)} → ${format(item.candidate[key].median)}`;
|
||||
const heapMetric = "retained_v8_heap_bytes_after_gc";
|
||||
const heaps = data.heap_diagnostics.summary;
|
||||
const changes = heaps.map(
|
||||
(item) =>
|
||||
(item.baseline[heapMetric].median - item.candidate[heapMetric].median) /
|
||||
2 ** 20,
|
||||
);
|
||||
const changesText = [...new Set(changes.map((value) => fixed(value, 1)))].join(
|
||||
"–",
|
||||
);
|
||||
const separated = heaps.every(
|
||||
(item) => item.baseline[heapMetric].min > item.candidate[heapMetric].max,
|
||||
);
|
||||
const provenance = `Baseline \`main@${data.measurement_inputs.baseline_revision}\`; candidate \`${data.measurement_inputs.candidate_revision}\`. This is a pinned comparison.\n\nIn separate inspector/forced-GC diagnostics, retained REA V8 heap decreases by about ${changesText} MiB. ${separated ? "The three-pair ranges do not overlap in either profile." : "Ranges overlap; inspect the observations below."} Inspector perturbation is included. Normal task timing, CPU and peak memory are reported separately; these observations do not establish a general latency or peak-memory reduction.`;
|
||||
const normal = table(
|
||||
[
|
||||
"Task",
|
||||
"Cache",
|
||||
"Input profile",
|
||||
"Wall s",
|
||||
"Family CPU s",
|
||||
"Family peak MiB",
|
||||
],
|
||||
data.summary.map((item) => [
|
||||
item.artifact ?? "directory",
|
||||
item.cache,
|
||||
item.profile,
|
||||
pair(item, "wall_seconds"),
|
||||
pair(item, "cpu_seconds"),
|
||||
pair(item, "kernel_family_peak_bytes_including_page_cache", mib),
|
||||
]),
|
||||
[3, 4, 5],
|
||||
);
|
||||
const discovery = data.summary.find(
|
||||
(item) =>
|
||||
item.phase === "discovery" &&
|
||||
item.cache === "cold" &&
|
||||
item.profile === "default",
|
||||
);
|
||||
assert.ok(discovery);
|
||||
const audit = (role) =>
|
||||
median(
|
||||
data.records
|
||||
.filter(
|
||||
(row) =>
|
||||
row.phase === "discovery" &&
|
||||
row.cache === "cold" &&
|
||||
row.profile === "default" &&
|
||||
row.role === role,
|
||||
)
|
||||
.map(
|
||||
(row) =>
|
||||
(row.runtime_audit_ms.before + row.runtime_audit_ms.after) / 1000,
|
||||
),
|
||||
);
|
||||
const sdk =
|
||||
`Cold/default directory audit wall medians are ${fixed(audit("baseline"))} s baseline / ${fixed(audit("candidate"))} s candidate; SDK spans are ${pair(discovery, "sdk_task_elapsed_seconds")} s. Whole-family costs include those experimental audits. Their cause and per-span CPU are unmeasured.\n\n${data.sdk_task_elapsed_scope}\n\n` +
|
||||
table(
|
||||
["Task", "Cache", "Inputs", "SDK span s, baseline → candidate"],
|
||||
data.summary.map((item) => [
|
||||
item.artifact ?? "directory",
|
||||
item.cache,
|
||||
item.profile,
|
||||
pair(item, "sdk_task_elapsed_seconds"),
|
||||
]),
|
||||
[3],
|
||||
);
|
||||
const heap = table(
|
||||
[
|
||||
"Inputs",
|
||||
"REA V8 retained MiB",
|
||||
"Baseline range",
|
||||
"Candidate range",
|
||||
"Diagnostic wall s",
|
||||
"CPU s",
|
||||
"Family peak MiB",
|
||||
],
|
||||
heaps.map((item) => [
|
||||
item.profile,
|
||||
pair(item, heapMetric, (value) => mib(value, 3)),
|
||||
...["baseline", "candidate"].map(
|
||||
(role) =>
|
||||
`${mib(item[role][heapMetric].min, 3)}–${mib(item[role][heapMetric].max, 3)}`,
|
||||
),
|
||||
pair(item, "wall_seconds"),
|
||||
pair(item, "cpu_seconds"),
|
||||
pair(item, "kernel_family_peak_bytes_including_page_cache", mib),
|
||||
]),
|
||||
[1, 2, 3, 4, 5, 6],
|
||||
);
|
||||
const integer = (value) => new Intl.NumberFormat("en-US").format(value);
|
||||
const tokens = (profile, phase, artifact) => {
|
||||
const values = data.records
|
||||
.filter(
|
||||
(row) =>
|
||||
row.profile === profile &&
|
||||
row.phase === phase &&
|
||||
row.artifact === artifact,
|
||||
)
|
||||
.map((row) => row.tokens_estimated.whole_task);
|
||||
assert.ok(values.length > 0);
|
||||
return [...new Set([Math.min(...values), Math.max(...values)])]
|
||||
.map(integer)
|
||||
.join("–");
|
||||
};
|
||||
const catalogs = data.catalogs;
|
||||
const context =
|
||||
`${data.tokenizer}. Actual model usage is unmeasured.\n\n` +
|
||||
table(
|
||||
[
|
||||
"Profile",
|
||||
"Input directory once",
|
||||
"Directory task",
|
||||
"ripgrep task",
|
||||
"Zellij task",
|
||||
],
|
||||
["default", "full"].map((profile) => [
|
||||
profile,
|
||||
integer(catalogs[profile].input_catalog_tokens),
|
||||
tokens(profile, "discovery", null),
|
||||
tokens(profile, "analysis", "ripgrep"),
|
||||
tokens(profile, "analysis", "zellij"),
|
||||
]),
|
||||
[1, 2, 3, 4],
|
||||
) +
|
||||
`\n\nRaw SDK directory costs per list: ${integer(catalogs.default.raw_catalog_tokens)}/${integer(catalogs.full.raw_catalog_tokens)} tokens and ${integer(catalogs.default.raw_catalog_bytes)}/${integer(catalogs.full.raw_catalog_bytes)} bytes for default/full.\n\n${data.context_scope}`;
|
||||
const sections = { provenance, normal, sdk, heap, context };
|
||||
// oxfmt may pad table cells and separator dashes; neither changes report facts.
|
||||
const normalized = (value) =>
|
||||
value
|
||||
.trim()
|
||||
.replace(/\s+/gu, " ")
|
||||
.replace(/\s*\|\s*/gu, "|")
|
||||
.replace(
|
||||
/(?<=\|)(:)?-{3,}(:)?(?=\|)/gu,
|
||||
(_, left, right) => `${left ?? ""}---${right ?? ""}`,
|
||||
);
|
||||
let rendered = original;
|
||||
const changed = [];
|
||||
for (const [name, content] of Object.entries(sections)) {
|
||||
const begin = `<!-- BEGIN:allocation-report:${name} -->`;
|
||||
const end = `<!-- END:allocation-report:${name} -->`;
|
||||
assert.equal(
|
||||
rendered.split(begin).length,
|
||||
2,
|
||||
`Expected one opening report marker: ${name}`,
|
||||
);
|
||||
assert.equal(
|
||||
rendered.split(end).length,
|
||||
2,
|
||||
`Expected one closing report marker: ${name}`,
|
||||
);
|
||||
const start = rendered.indexOf(begin) + begin.length;
|
||||
const finish = rendered.indexOf(end);
|
||||
assert.ok(finish > start, `Invalid report marker order: ${name}`);
|
||||
if (normalized(rendered.slice(start, finish)) !== normalized(content)) {
|
||||
changed.push(name);
|
||||
rendered =
|
||||
rendered.slice(0, start) + `\n\n${content}\n\n` + rendered.slice(finish);
|
||||
}
|
||||
}
|
||||
if (values.check) {
|
||||
assert.equal(
|
||||
changed.length,
|
||||
0,
|
||||
`Generated allocation report differs: ${changed.join(", ")}`,
|
||||
);
|
||||
console.log("Allocation report matches committed receipt data");
|
||||
} else if (changed.length) {
|
||||
await writeFile(markdownPath, rendered);
|
||||
console.log(`Updated allocation report sections: ${changed.join(", ")}`);
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// Diagnostic only: inspect the explicitly launched REA process after its task.
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
export const sampleRetainedHeap = async (endpoint, expectedPid) => {
|
||||
const url = new URL(endpoint);
|
||||
assert.equal(url.protocol, "ws:");
|
||||
assert.equal(url.hostname, "127.0.0.1");
|
||||
assert.ok(Number.isSafeInteger(expectedPid) && expectedPid > 0);
|
||||
const socket = new WebSocket(url);
|
||||
let sequence = 0;
|
||||
const pending = new Map();
|
||||
const closed = new Promise((resolve) => {
|
||||
socket.addEventListener("close", resolve, { once: true });
|
||||
});
|
||||
const rejectPending = (cause) => {
|
||||
for (const request of pending.values()) {
|
||||
clearTimeout(request.timer);
|
||||
request.reject(cause);
|
||||
}
|
||||
pending.clear();
|
||||
};
|
||||
socket.addEventListener("error", () =>
|
||||
rejectPending(new Error("Owned REA inspector connection failed")),
|
||||
);
|
||||
socket.addEventListener("close", () =>
|
||||
rejectPending(new Error("Owned REA inspector connection closed")),
|
||||
);
|
||||
socket.addEventListener("message", (event) => {
|
||||
try {
|
||||
const reply = JSON.parse(event.data);
|
||||
const request = pending.get(reply.id);
|
||||
if (request === undefined) return;
|
||||
pending.delete(reply.id);
|
||||
clearTimeout(request.timer);
|
||||
if (reply.error !== undefined)
|
||||
request.reject(new Error(JSON.stringify(reply.error)));
|
||||
else request.resolve(reply.result);
|
||||
} catch (cause) {
|
||||
rejectPending(cause);
|
||||
}
|
||||
});
|
||||
const withinDeadline = async (promise) => {
|
||||
let timer;
|
||||
try {
|
||||
return await Promise.race([
|
||||
promise,
|
||||
new Promise((_, reject) => {
|
||||
timer = setTimeout(
|
||||
() => reject(new Error("Owned REA inspector deadline exceeded")),
|
||||
10_000,
|
||||
);
|
||||
}),
|
||||
]);
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
const call = (method, params = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const id = ++sequence;
|
||||
const timer = setTimeout(() => {
|
||||
pending.delete(id);
|
||||
reject(new Error(`Owned REA inspector timed out: ${method}`));
|
||||
}, 10_000);
|
||||
pending.set(id, { resolve, reject, timer });
|
||||
try {
|
||||
socket.send(JSON.stringify({ id, method, params }));
|
||||
} catch (cause) {
|
||||
pending.delete(id);
|
||||
clearTimeout(timer);
|
||||
reject(cause);
|
||||
}
|
||||
});
|
||||
try {
|
||||
await withinDeadline(
|
||||
new Promise((resolve, reject) => {
|
||||
socket.addEventListener("open", resolve, { once: true });
|
||||
socket.addEventListener("error", reject, { once: true });
|
||||
socket.addEventListener("close", reject, { once: true });
|
||||
}),
|
||||
);
|
||||
const identity = await call("Runtime.evaluate", {
|
||||
expression: "process.pid",
|
||||
returnByValue: true,
|
||||
});
|
||||
assert.equal(identity.result.value, expectedPid);
|
||||
const before = await call("Runtime.getHeapUsage");
|
||||
await call("HeapProfiler.collectGarbage");
|
||||
const after = await call("Runtime.getHeapUsage");
|
||||
for (const sample of [before, after]) {
|
||||
assert.ok(Number.isSafeInteger(sample.usedSize) && sample.usedSize >= 0);
|
||||
assert.ok(Number.isSafeInteger(sample.totalSize));
|
||||
assert.ok(sample.totalSize >= sample.usedSize);
|
||||
}
|
||||
return {
|
||||
scope:
|
||||
"Owned REA V8 heap after the complete SDK task; inspector and forced GC are diagnostic overhead",
|
||||
pid_verified: true,
|
||||
before_gc: before,
|
||||
after_gc: after,
|
||||
};
|
||||
} finally {
|
||||
rejectPending(new Error("Owned REA inspector diagnostic finished"));
|
||||
socket.close();
|
||||
await withinDeadline(closed);
|
||||
}
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import argparse
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import math
|
||||
import statistics
|
||||
from pathlib import Path
|
||||
|
||||
@@ -32,7 +33,8 @@ spec.loader.exec_module(module)
|
||||
catalogs = {}
|
||||
identities = {}
|
||||
rows = []
|
||||
for case in inputs['cases']:
|
||||
heap_rows = []
|
||||
for case in inputs['cases'] + inputs.get('diagnostic_cases', []):
|
||||
directory = root / case['case']
|
||||
report = json.loads((directory / 'report.json').read_text())
|
||||
assert report['status'] == 'passed' and report['cleanup_verified']
|
||||
@@ -41,6 +43,7 @@ for case in inputs['cases']:
|
||||
for name, producer_digest in report['producer_sources_sha256'].items():
|
||||
assert producer_digest == inputs['harness_sources_sha256'][name]
|
||||
assert report['runtime_binding'] == expected[case['role']]
|
||||
assert type(report['elapsed_ms']) in [int, float] and math.isfinite(report['elapsed_ms']) and report['elapsed_ms'] >= 0
|
||||
for key in ['role', 'cache', 'profile', 'phase']:
|
||||
assert report[key] == case[key]
|
||||
raw = (directory / 'catalog.json').read_bytes()
|
||||
@@ -82,6 +85,7 @@ for case in inputs['cases']:
|
||||
assert guard_digest == inputs['harness_sources_sha256'][name]
|
||||
del resource['guard_sources_sha256']
|
||||
row = {**case, 'runtime_binding_sha256': sha(compact(report['runtime_binding']).encode()), 'runtime_audit_ms': report['runtime_audit_ms'],
|
||||
'sdk_task_elapsed_seconds': report['elapsed_ms'] / 1000,
|
||||
'catalog_sha256': catalog_digest, 'parent_evidence_id': report.get('parent_evidence_id'),
|
||||
'parent_semantic_sha256': report.get('parent_semantic_sha256'),
|
||||
'answer_sha256': sha(compact(report['answer']).encode()), 'tokens_estimated': tokens,
|
||||
@@ -91,7 +95,19 @@ for case in inputs['cases']:
|
||||
**resource,
|
||||
'receipt_sha256': {name: sha((directory / name).read_bytes()) for name in ['report.json', 'catalog.json', 'server.json', 'transcript.json']}}
|
||||
assert row['catalog_sha256'] == row['receipt_sha256']['catalog.json'] == catalogs[profile]['sha256']
|
||||
rows.append(row)
|
||||
heap = report.get('diagnostic_heap')
|
||||
if case.get('diagnostic_heap'):
|
||||
assert case['phase'] == 'analysis' and case['cache'] == 'cold' and case['artifact'] == 'zellij'
|
||||
assert heap is not None and heap['pid_verified'] is True
|
||||
for sample in [heap['before_gc'], heap['after_gc']]:
|
||||
assert type(sample['usedSize']) is int and sample['usedSize'] >= 0
|
||||
assert type(sample['totalSize']) is int and sample['totalSize'] >= sample['usedSize']
|
||||
row['heap_observation'] = heap
|
||||
row['retained_v8_heap_bytes_after_gc'] = heap['after_gc']['usedSize']
|
||||
heap_rows.append(row)
|
||||
else:
|
||||
assert heap is None, 'Diagnostic GC contaminated a normal task trial'
|
||||
rows.append(row)
|
||||
assert len(rows) == 72
|
||||
summary = []
|
||||
for phase in ['discovery', 'analysis']:
|
||||
@@ -104,7 +120,7 @@ for phase in ['discovery', 'analysis']:
|
||||
assert len(selected) == 3
|
||||
item[role] = {metric: {'median': statistics.median(row[metric] for row in selected),
|
||||
'min': min(row[metric] for row in selected), 'max': max(row[metric] for row in selected)}
|
||||
for metric in ['wall_seconds', 'cpu_seconds', 'kernel_family_peak_bytes_including_page_cache']}
|
||||
for metric in ['wall_seconds', 'cpu_seconds', 'kernel_family_peak_bytes_including_page_cache', 'sdk_task_elapsed_seconds']}
|
||||
item[role]['tokens_estimated'] = [row['tokens_estimated']['whole_task'] for row in selected]
|
||||
summary.append(item)
|
||||
result = {'status': 'passed', 'tokenizer': f'tiktoken=={tiktoken.__version__}/cl100k_base; estimates, not model usage',
|
||||
@@ -113,6 +129,23 @@ result = {'status': 'passed', 'tokenizer': f'tiktoken=={tiktoken.__version__}/cl
|
||||
'cache_scope': 'Cold disables server Node compile cache. Warm enables a prepopulated role/profile-specific cache after one complete analysis priming run. Fresh server/client processes and HOME each trial. Driver compile cache disabled in both cohorts. Cache hit rate and OS page cache are unmeasured; source/runtime audits warm compiled-file pages before startup.',
|
||||
'resource_scope': 'Whole SDK driver, resource observer and owned REA process family. Includes before/after source-and-all-compiled-JS/JSON binding, SDK schema checks, extra validation calls, bundle export and independent oracle checking. Kernel cgroup peak includes file cache. Build/download/oracle preparation and cache priming excluded. Retained V8 heap and forced-GC diagnostics were not separately measured.',
|
||||
'measurement_inputs': inputs, 'catalogs': catalogs, 'summary': summary, 'records': rows,
|
||||
'sdk_task_elapsed_scope': 'SDK span from client connect through task checks and client/server close. Excludes pre/post runtime binding and preflight artifact/oracle loading. Diagnostic spans include inspector and forced GC. CPU is measured for the whole family only; per-span CPU is unmeasured.',
|
||||
'warmup_receipts': {name: sha((root / name / 'report.json').read_bytes()) for name in [f'warmup-{profile}-{role}' for profile in ['default', 'full'] for role in ['baseline', 'candidate']]}}
|
||||
if heap_rows:
|
||||
assert len(heap_rows) == 12
|
||||
heap_summary = []
|
||||
for profile in ['default', 'full']:
|
||||
item = {'profile': profile}
|
||||
for role in ['baseline', 'candidate']:
|
||||
selected = [row for row in heap_rows if row['profile'] == profile and row['role'] == role]
|
||||
assert len(selected) == 3
|
||||
item[role] = {metric: {'median': statistics.median(row[metric] for row in selected),
|
||||
'min': min(row[metric] for row in selected), 'max': max(row[metric] for row in selected)}
|
||||
for metric in ['retained_v8_heap_bytes_after_gc', 'wall_seconds', 'cpu_seconds', 'sdk_task_elapsed_seconds',
|
||||
'kernel_family_peak_bytes_including_page_cache']}
|
||||
heap_summary.append(item)
|
||||
result['heap_diagnostics'] = {
|
||||
'scope': 'Separate three-pair Zellij tasks per profile, with an explicitly launched loopback inspector. Four CDP calls verify the owned REA PID, read heap, force GC and read heap again after the completed SDK task. These whole-family costs include inspector and diagnostic GC overhead; they are excluded from the normal timing cohort. Heap values describe REA V8 only, not the client or the whole process family.',
|
||||
'summary': heap_summary, 'records': heap_rows}
|
||||
args.output.write_text(json.dumps(result, indent=2) + '\n')
|
||||
print(json.dumps(summary, indent=2))
|
||||
@@ -238,7 +238,9 @@ export const providerCatalog = (sources) => {
|
||||
identity: sources.pwntoolsProvider.PWNTOOLS_PROVIDER_IDENTITY,
|
||||
contracts:
|
||||
sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name !== "inspect_pe_resources",
|
||||
({ name }) =>
|
||||
name !== "inspect_pe_resources" &&
|
||||
name !== "inspect_pe_certificates",
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -248,6 +250,13 @@ export const providerCatalog = (sources) => {
|
||||
({ name }) => name === "inspect_pe_resources",
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.artifactProviders.PE_CERTIFICATES_PROVIDER,
|
||||
contracts:
|
||||
sources.binaryDiagnosticsContracts.BINARY_DIAGNOSTICS_TOOL_CONTRACTS.filter(
|
||||
({ name }) => name === "inspect_pe_certificates",
|
||||
),
|
||||
},
|
||||
{
|
||||
identity: sources.captureProvider.TSHARK_PROVIDER_IDENTITY,
|
||||
contracts: sources.captureContracts.CAPTURE_TOOL_CONTRACTS,
|
||||
|
||||
@@ -3,6 +3,26 @@ import { dirname, join, relative } from "node:path";
|
||||
|
||||
import { exec, execNpm, pathExists } from "./lib/verify-package-core.mjs";
|
||||
|
||||
/** Install the separately locked Skills CLI so verification resolves no registry metadata. */
|
||||
const lockedSkillsCli = async (root, workspace) => {
|
||||
const tool = join(workspace, "skills-cli");
|
||||
await mkdir(tool, { recursive: true });
|
||||
for (const file of ["package.json", "package-lock.json"])
|
||||
await cp(join(root, "scripts/verify-tools/skills", file), join(tool, file));
|
||||
await execNpm(
|
||||
["ci", "--prefer-offline", "--ignore-scripts", "--no-audit", "--no-fund"],
|
||||
{ cwd: tool, timeout: 120_000 },
|
||||
);
|
||||
const installed = join(tool, "node_modules/skills");
|
||||
const manifest = JSON.parse(
|
||||
await readFile(join(installed, "package.json"), "utf8"),
|
||||
);
|
||||
return {
|
||||
version: manifest.version,
|
||||
entry: join(installed, manifest.bin.skills),
|
||||
};
|
||||
};
|
||||
|
||||
/** Exercise Skills' default root discovery without ignored build outputs. */
|
||||
export async function verifyRepositorySkill({ root, workspace }) {
|
||||
const snapshot = join(workspace, "repository");
|
||||
@@ -33,15 +53,14 @@ export async function verifyRepositorySkill({ root, workspace }) {
|
||||
throw new Error("repository skill check must run without generated skills");
|
||||
await mkdir(consumer, { recursive: true });
|
||||
await mkdir(home, { recursive: true });
|
||||
const skillsVersion = "1.7.2";
|
||||
await execNpm(
|
||||
const { version: skillsVersion, entry } = await lockedSkillsCli(
|
||||
root,
|
||||
workspace,
|
||||
);
|
||||
await exec(
|
||||
process.execPath,
|
||||
[
|
||||
"exec",
|
||||
"--yes",
|
||||
"--package",
|
||||
`skills@${skillsVersion}`,
|
||||
"--",
|
||||
"skills",
|
||||
entry,
|
||||
"add",
|
||||
snapshot,
|
||||
"--skill",
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"name": "rea-verify-skills-cli",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rea-verify-skills-cli",
|
||||
"dependencies": {
|
||||
"skills": "1.7.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/fs-minipass": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz",
|
||||
"integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"minipass": "^7.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz",
|
||||
"integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz",
|
||||
"integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/minizlib": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz",
|
||||
"integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/skills": {
|
||||
"version": "1.7.2",
|
||||
"resolved": "https://registry.npmjs.org/skills/-/skills-1.7.2.tgz",
|
||||
"integrity": "sha512-j+GzyF/oNahsjjjup6y3lJpFkC1lQjIkf4kAMXICA1hboBqi0fAGXAGoE4x9FRZiP1br/YE9+L3/Tqq6PIAe0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tar": "^7.5.20",
|
||||
"yaml": "^2.8.3"
|
||||
},
|
||||
"bin": {
|
||||
"add-skill": "bin/cli.mjs",
|
||||
"skills": "bin/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22.20.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.22",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
|
||||
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
"chownr": "^3.0.0",
|
||||
"minipass": "^7.1.2",
|
||||
"minizlib": "^3.1.0",
|
||||
"yallist": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz",
|
||||
"integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.9.1",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz",
|
||||
"integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/eemeli"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "rea-verify-skills-cli",
|
||||
"private": true,
|
||||
"description": "Lockfile-pinned Skills CLI for the repository skill check in verify:package.",
|
||||
"dependencies": {
|
||||
"skills": "1.7.2"
|
||||
}
|
||||
}
|
||||
@@ -4,14 +4,14 @@ import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
|
||||
import { dexDescriptorSchema } from "../domain/android/androidAnalysis.js";
|
||||
|
||||
const count = z.number().int().nonnegative();
|
||||
const count = z.int().nonnegative();
|
||||
/** Typed allocation failures authored before upstream erases the JVM exception. */
|
||||
const jadxMemoryFailureSchema = z.strictObject({
|
||||
kind: z.literal("jvm-memory-failure"),
|
||||
memory_area: z.enum(["heap", "unknown"]),
|
||||
exception_type: z.literal("java.lang.OutOfMemoryError"),
|
||||
message: z.string().optional(),
|
||||
max_heap_bytes: z.number().int().positive().safe(),
|
||||
max_heap_bytes: z.int().positive().safe(),
|
||||
});
|
||||
/** Selector failures authored by REA's bridge, separate from arbitrary engine errors. */
|
||||
const jadxInputFailureSchema = z.strictObject({
|
||||
@@ -40,8 +40,8 @@ export const parseJadxFailure = (
|
||||
/** Values observed inside the exact JVM producing the analysis. */
|
||||
export const jadxRuntimeSchema = z.object({
|
||||
engine_reported_version: z.string().min(1),
|
||||
max_heap_bytes: z.number().int().positive().safe(),
|
||||
available_processors: z.number().int().positive(),
|
||||
max_heap_bytes: z.int().positive().safe(),
|
||||
available_processors: z.int().positive(),
|
||||
java_version: z.string().min(1),
|
||||
metadata_scope: z.literal("parsed_members"),
|
||||
});
|
||||
@@ -81,11 +81,11 @@ export const jadxClassSchema = z.object({
|
||||
signature: z.string(),
|
||||
dex_descriptor: dexDescriptorSchema,
|
||||
is_constructor: z.boolean(),
|
||||
def_pos: z.number().int(),
|
||||
def_pos: z.int(),
|
||||
}),
|
||||
),
|
||||
fields: z.array(
|
||||
z.object({ name: z.string(), type: z.string(), def_pos: z.number().int() }),
|
||||
z.object({ name: z.string(), type: z.string(), def_pos: z.int() }),
|
||||
),
|
||||
inner_classes: z.array(z.string()),
|
||||
});
|
||||
@@ -115,7 +115,7 @@ export const jadxXrefsSchema = z.object({
|
||||
full_name: z.string(),
|
||||
containing_class: z.string(),
|
||||
top_class: z.string(),
|
||||
def_pos: z.number().int(),
|
||||
def_pos: z.int(),
|
||||
direct_invokes_exhaustive: z.boolean().optional(),
|
||||
direct_invokes: z
|
||||
.array(
|
||||
|
||||
@@ -499,6 +499,8 @@ const workflowAvailabilityFor = ({
|
||||
};
|
||||
if (name === "inspect_pe_resources")
|
||||
return { reason: "available", remediation: null };
|
||||
if (name === "inspect_pe_certificates")
|
||||
return { reason: "available", remediation: null };
|
||||
if (name === "inspect_evm_interface") {
|
||||
if (policy.evmInterfaceEnabled !== true)
|
||||
return {
|
||||
|
||||
@@ -198,12 +198,13 @@ export class EnhancedTools {
|
||||
): EnhancedResult {
|
||||
switch (call.name) {
|
||||
case "inspect_native_dispatch_metadata":
|
||||
return this.#inspectNativeDispatchMetadata(
|
||||
call.input.max_records,
|
||||
return inspectNativeDispatch(this.analysis, call.input.max_records, {
|
||||
signal,
|
||||
call.input.objc_class_import_addresses,
|
||||
call.input.objc_class_import_addresses_complete,
|
||||
);
|
||||
objcClassImportAddresses: call.input.objc_class_import_addresses,
|
||||
objcClassImportAddressesComplete:
|
||||
call.input.objc_class_import_addresses_complete,
|
||||
maxInputBytes: call.input.max_input_bytes,
|
||||
});
|
||||
case "get_objc_classes":
|
||||
return this.#objcClasses(call.input.pattern, signal);
|
||||
case "get_objc_protocols":
|
||||
@@ -420,21 +421,6 @@ export class EnhancedTools {
|
||||
);
|
||||
}
|
||||
|
||||
#inspectNativeDispatchMetadata(
|
||||
maxRecords: number,
|
||||
signal?: AbortSignal,
|
||||
objcClassImportAddresses?: readonly string[],
|
||||
objcClassImportAddressesComplete?: boolean,
|
||||
): EnhancedResult {
|
||||
return inspectNativeDispatch(
|
||||
this.analysis,
|
||||
maxRecords,
|
||||
signal,
|
||||
objcClassImportAddresses,
|
||||
objcClassImportAddressesComplete,
|
||||
);
|
||||
}
|
||||
|
||||
async #objcProtocols(signal?: AbortSignal): EnhancedResult {
|
||||
const names = await this.#allAddressed("list_names", signal);
|
||||
return names.ok ? ok(discoverObjcProtocols(names.value)) : names;
|
||||
|
||||
@@ -48,6 +48,13 @@ export const PE_RESOURCES_PROVIDER = Object.freeze({
|
||||
version: "1",
|
||||
} as const);
|
||||
|
||||
/** Provider identity for portable, execution-free PE Authenticode table decoding. */
|
||||
export const PE_CERTIFICATES_PROVIDER = Object.freeze({
|
||||
id: "rea-pe-certificates",
|
||||
name: "REA PE certificate parser",
|
||||
version: "1",
|
||||
} as const);
|
||||
|
||||
/** Provider identity for deterministic managed-code comparison workflows. */
|
||||
export const MANAGED_WORKFLOW_PROVIDER = {
|
||||
id: "rea-dotnet-workflows",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import type {
|
||||
ExecutionOptions,
|
||||
ProviderIdentity,
|
||||
} from "../AnalysisProvider.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type {
|
||||
InspectPeCertificatesInput,
|
||||
PeCertificates,
|
||||
} from "../../domain/native/peCertificates.js";
|
||||
import type { Result } from "../../domain/result.js";
|
||||
|
||||
/** Stable read and PE certificate-table decoding, independent of a disassembler session. */
|
||||
export interface PeCertificatesPort {
|
||||
readonly identity: ProviderIdentity;
|
||||
close?(): Promise<void>;
|
||||
inspect(
|
||||
input: InspectPeCertificatesInput,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<PeCertificates, AnalysisError>>;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { PeCertificatesService } from "./PeCertificatesService.js";
|
||||
import { PE_CERTIFICATES_PROVIDER } from "../InvestigationProviders.js";
|
||||
import type { PeCertificates } from "../../domain/native/peCertificates.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
|
||||
const identity = PE_CERTIFICATES_PROVIDER;
|
||||
const report: PeCertificates = {
|
||||
artifact: {
|
||||
path: "/artifacts/app.exe",
|
||||
sha256: "a".repeat(64),
|
||||
bytes: 512,
|
||||
},
|
||||
format: "pe32",
|
||||
machine: 0x14c,
|
||||
table: null,
|
||||
entries: [],
|
||||
coverage: { status: "complete", examined_entries: 0, certificates: 0 },
|
||||
limitations: ["IMAGE_DIRECTORY_ENTRY_SECURITY is absent."],
|
||||
};
|
||||
|
||||
it("rejects a relative path and extra arguments before reading", async () => {
|
||||
let calls = 0;
|
||||
const service = new PeCertificatesService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
calls += 1;
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
const relative = await service.inspect({ path: "app.exe" });
|
||||
const extra = await service.inspect({
|
||||
path: report.artifact.path,
|
||||
extra: true,
|
||||
});
|
||||
expect(relative.ok).toBe(false);
|
||||
expect(extra.ok).toBe(false);
|
||||
if (!relative.ok) expect(relative.error).toBeInstanceOf(AnalysisInputError);
|
||||
if (!extra.ok) expect(extra.error).toBeInstanceOf(AnalysisInputError);
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
it("records the selected PE and an absent certificate table", async () => {
|
||||
const result = await new PeCertificatesService({
|
||||
identity,
|
||||
inspect: () => Promise.resolve(ok(report)),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value).toMatchObject({
|
||||
confidence: "observed",
|
||||
subject: { format: "pe", architecture: "x86" },
|
||||
normalized_result: report,
|
||||
});
|
||||
});
|
||||
|
||||
it("discards an observation if the caller cancels while the provider completes", async () => {
|
||||
const controller = new AbortController();
|
||||
const service = new PeCertificatesService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
controller.abort();
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
const result = await service.inspect(
|
||||
{ path: report.artifact.path },
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisCancelledError);
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import { isAbsolute } from "node:path";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { PeCertificatesPort } from "./PeCertificatesPort.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
inspectPeCertificatesInputSchema,
|
||||
peCertificatesSchema,
|
||||
type PeCertificates,
|
||||
} from "../../domain/native/peCertificates.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import {
|
||||
decodePeMachine,
|
||||
peOptionalHeaderMagic,
|
||||
type PeArchitecture,
|
||||
} from "../../domain/peMachine.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
|
||||
|
||||
const OPERATION = "inspect_pe_certificates";
|
||||
|
||||
/** Shared caller validation, output parsing and Evidence provenance for CLI/MCP. */
|
||||
export class PeCertificatesService {
|
||||
constructor(readonly provider: PeCertificatesPort) {}
|
||||
|
||||
close(): Promise<void> {
|
||||
return this.provider.close?.() ?? Promise.resolve();
|
||||
}
|
||||
|
||||
async inspect(
|
||||
rawInput: unknown,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
if (options?.signal?.aborted)
|
||||
return err(new AnalysisCancelledError(OPERATION));
|
||||
const input = inspectPeCertificatesInputSchema.safeParse(rawInput);
|
||||
if (!input.success)
|
||||
return err(
|
||||
analysisInputErrorFromIssues(OPERATION, input.error.issues, rawInput, {
|
||||
cause: input.error,
|
||||
}),
|
||||
);
|
||||
if (!isAbsolute(input.data.path))
|
||||
return err(
|
||||
new AnalysisInputError(OPERATION, undefined, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: "Expected an absolute filesystem path on this host.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const result = await this.provider.inspect(input.data, options);
|
||||
if (!result.ok) return result;
|
||||
const report = peCertificatesSchema.safeParse(result.value);
|
||||
if (
|
||||
!report.success ||
|
||||
report.data.artifact.path !== input.data.path ||
|
||||
!validReport(
|
||||
report.data,
|
||||
input.data.max_entries,
|
||||
input.data.max_file_bytes,
|
||||
)
|
||||
)
|
||||
return err(
|
||||
new AnalysisOutputError(
|
||||
OPERATION,
|
||||
"PE certificate provider returned malformed output or changed artifact identity.",
|
||||
),
|
||||
);
|
||||
const value = report.data;
|
||||
const architecture = subjectArchitecture(value);
|
||||
const locations = [
|
||||
{ kind: "artifact-path" as const, path: value.artifact.path },
|
||||
...(value.table === null
|
||||
? []
|
||||
: [
|
||||
{
|
||||
kind: "file-offset-range" as const,
|
||||
start: value.table.location.offset,
|
||||
end: value.table.location.offset + value.table.location.bytes,
|
||||
},
|
||||
]),
|
||||
];
|
||||
return completeProviderEvidence(
|
||||
createEvidence(
|
||||
{
|
||||
path: value.artifact.path,
|
||||
format: "pe",
|
||||
sha256: value.artifact.sha256,
|
||||
...(architecture === undefined ? {} : { architecture }),
|
||||
},
|
||||
this.provider.identity,
|
||||
{
|
||||
operation: OPERATION,
|
||||
parameters: input.data,
|
||||
result: value,
|
||||
confidence: "observed",
|
||||
limitations: value.limitations,
|
||||
locations,
|
||||
},
|
||||
),
|
||||
options?.signal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const subjectArchitecture = (
|
||||
report: PeCertificates,
|
||||
): PeArchitecture | undefined => {
|
||||
const machine = decodePeMachine(report.machine);
|
||||
if (machine === undefined || machine.nativeOsOverride) return undefined;
|
||||
const magic = report.format === "pe32-plus" ? 0x20b : 0x10b;
|
||||
return magic === peOptionalHeaderMagic(machine.architecture)
|
||||
? machine.architecture
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const validReport = (
|
||||
report: PeCertificates,
|
||||
maxEntries: number,
|
||||
maxBytes: number,
|
||||
): boolean => {
|
||||
const inside = ({ offset, bytes }: { offset: number; bytes: number }) =>
|
||||
offset <= report.artifact.bytes - bytes;
|
||||
if (
|
||||
report.artifact.bytes > maxBytes ||
|
||||
report.coverage.examined_entries > maxEntries ||
|
||||
report.coverage.certificates !==
|
||||
report.entries.reduce(
|
||||
(count, entry) => count + (entry.pkcs7?.certificates.length ?? 0),
|
||||
0,
|
||||
) ||
|
||||
report.entries.length > maxEntries
|
||||
)
|
||||
return false;
|
||||
if (report.table !== null && !inside(report.table.location)) return false;
|
||||
return report.entries.every(
|
||||
(entry) => inside(entry.location) && inside(entry.payload.location),
|
||||
);
|
||||
};
|
||||
@@ -5,20 +5,51 @@ import {
|
||||
analysisViewCaptureFixture,
|
||||
environmentManifestCaptureFixture,
|
||||
} from "../../../tests/fixtures/analysisView.js";
|
||||
import { scenarioCaptureEvidence } from "../../../tests/fixtures/browserScenarioCapture.js";
|
||||
import {
|
||||
environmentServeFixture,
|
||||
environmentServeSource,
|
||||
} from "../../../tests/fixtures/environmentServe.js";
|
||||
import { AnalysisInputError } from "../../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { inspectAnalysisView } from "../analysisView/AnalysisViewService.js";
|
||||
import { loadEnvironmentSource } from "./environmentSource.js";
|
||||
|
||||
const refusal = (manifest: unknown, evidence: unknown) => {
|
||||
const loaded = loadEnvironmentSource(manifest, evidence);
|
||||
const refusal = (manifest: unknown, evidence: unknown, operation?: string) => {
|
||||
const loaded = loadEnvironmentSource(manifest, evidence, operation);
|
||||
if (loaded.ok) throw new Error("Expected the pair to be refused");
|
||||
return projectAnalysisError(loaded.error);
|
||||
};
|
||||
|
||||
/** Authenticated Evidence whose payload is rewritten; createEvidence recomputes the digest. */
|
||||
const resealed = (
|
||||
evidence: Evidence,
|
||||
edit: (result: Record<string, unknown>) => void,
|
||||
operation: string = evidence.operation,
|
||||
): Evidence => {
|
||||
const result = structuredClone(evidence.normalized_result) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
edit(result);
|
||||
return createEvidence(undefined, evidence.provider, {
|
||||
operation,
|
||||
parameters: structuredClone(evidence.parameters),
|
||||
result: result as never,
|
||||
confidence: evidence.confidence,
|
||||
});
|
||||
};
|
||||
|
||||
/** Re-cite a manifest's parent so pairing passes and the capture parse alone decides. */
|
||||
const citing = (manifest: Evidence, capture: Evidence): Evidence =>
|
||||
resealed(manifest, (result) => {
|
||||
result.parent_evidence_id = capture.evidence_id;
|
||||
});
|
||||
|
||||
const record = (value: unknown, key: string): Record<string, unknown> =>
|
||||
(value as Record<string, Record<string, unknown>>)[key] ?? {};
|
||||
|
||||
describe("loadEnvironmentSource", () => {
|
||||
it("authenticates the pair and indexes every cited record as a replayable exchange", () => {
|
||||
const fixture = environmentServeFixture();
|
||||
@@ -165,6 +196,111 @@ describe("loadEnvironmentSource", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadEnvironmentSource payload attribution", () => {
|
||||
it("reports capture payload failures beneath normalized_result instead of the supported operation", () => {
|
||||
const fixture = environmentServeFixture();
|
||||
for (const operation of [
|
||||
"inspect_web_network_capture",
|
||||
"capture_browser_scenario",
|
||||
]) {
|
||||
const capture = createEvidence(undefined, fixture.capture.provider, {
|
||||
operation,
|
||||
parameters: {},
|
||||
result: {},
|
||||
confidence: "observed",
|
||||
});
|
||||
for (const command of ["environment-serve", "environment-export"]) {
|
||||
const loaded = loadEnvironmentSource(
|
||||
citing(fixture.manifest, capture),
|
||||
capture,
|
||||
command,
|
||||
);
|
||||
if (loaded.ok || !(loaded.error instanceof AnalysisInputError))
|
||||
throw new Error("Expected an input refusal");
|
||||
expect(loaded.error.issues.length).toBeGreaterThan(0);
|
||||
for (const issue of loaded.error.issues)
|
||||
expect(issue.path.slice(0, 2)).toEqual([
|
||||
"evidence",
|
||||
"normalized_result",
|
||||
]);
|
||||
expect(loaded.error.cause).toBeInstanceOf(Error);
|
||||
expect(projectAnalysisError(loaded.error)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
details: { operation: command },
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("reports the full path of a nested capture payload field", () => {
|
||||
const fixture = environmentServeFixture();
|
||||
const har = resealed(fixture.capture, (result) => {
|
||||
record(result, "artifact").sha256 = 42;
|
||||
});
|
||||
expect(refusal(citing(fixture.manifest, har), har).details?.issues).toEqual(
|
||||
[
|
||||
expect.objectContaining({
|
||||
path: ["evidence", "normalized_result", "artifact", "sha256"],
|
||||
reason: "invalid_type",
|
||||
}),
|
||||
],
|
||||
);
|
||||
const scenario = resealed(scenarioCaptureEvidence(), (result) => {
|
||||
record(result, "browser").product = 7;
|
||||
});
|
||||
expect(
|
||||
refusal(citing(fixture.manifest, scenario), scenario).details?.issues,
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
path: ["evidence", "normalized_result", "browser", "product"],
|
||||
reason: "invalid_type",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("still blames the operation of Evidence no capture schema owns", () => {
|
||||
const fixture = environmentServeFixture();
|
||||
const other = resealed(fixture.capture, () => {}, "inspect_other_capture");
|
||||
expect(
|
||||
refusal(citing(fixture.manifest, other), other).details?.issues,
|
||||
).toEqual([
|
||||
expect.objectContaining({
|
||||
path: ["evidence", "operation"],
|
||||
expected: "inspect_web_network_capture or capture_browser_scenario",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("separates the manifest operation, view kind and view payload failures", () => {
|
||||
const fixture = environmentServeFixture();
|
||||
expect(refusal(fixture.capture, fixture.capture).details?.issues).toEqual([
|
||||
expect.objectContaining({
|
||||
path: ["manifest", "operation"],
|
||||
expected: "inspect_analysis_view",
|
||||
}),
|
||||
]);
|
||||
const malformed = resealed(fixture.manifest, (result) => {
|
||||
result.name = 7;
|
||||
});
|
||||
expect(refusal(malformed, fixture.capture).details?.issues).toEqual([
|
||||
expect.objectContaining({
|
||||
path: ["manifest", "normalized_result", "name"],
|
||||
reason: "invalid_type",
|
||||
}),
|
||||
]);
|
||||
const unknownKind = resealed(fixture.manifest, (result) => {
|
||||
result.kind = "not-a-view";
|
||||
});
|
||||
expect(refusal(unknownKind, fixture.capture).details?.issues).toEqual([
|
||||
expect.objectContaining({
|
||||
path: ["manifest", "normalized_result", "kind"],
|
||||
expected: "environment-manifest",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("loadEnvironmentSource recorded request lines", () => {
|
||||
it("keeps the delimiter of an explicit empty query in the recorded exchange", () => {
|
||||
const capture = environmentManifestCaptureFixture();
|
||||
|
||||
@@ -143,18 +143,34 @@ export const parseEvidenceFile = (
|
||||
} catch (cause: unknown) {
|
||||
if (!(cause instanceof z.ZodError)) throw cause;
|
||||
return err(
|
||||
inputError(
|
||||
schemaInputError(
|
||||
operation,
|
||||
projectInputIssues(cause.issues, value).map((issue) => ({
|
||||
...issue,
|
||||
path: [role, ...issue.path],
|
||||
})),
|
||||
{ prefix: [role], cause, value },
|
||||
`The ${role} file is not authenticated REA Evidence JSON.`,
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** A schema refusal of `value`, found at `prefix`; issue paths extend the prefix and the schema failure is retained. */
|
||||
const schemaInputError = (
|
||||
operation: string,
|
||||
failure: {
|
||||
readonly prefix: readonly (string | number)[];
|
||||
readonly cause: z.ZodError;
|
||||
readonly value: unknown;
|
||||
},
|
||||
userMessage: string,
|
||||
): AnalysisInputError =>
|
||||
new AnalysisInputError(
|
||||
operation,
|
||||
{ userMessage, cause: failure.cause },
|
||||
projectInputIssues(failure.cause.issues, failure.value).map((issue) => ({
|
||||
...issue,
|
||||
path: [...failure.prefix, ...issue.path],
|
||||
})),
|
||||
);
|
||||
|
||||
const recordedExchange = (
|
||||
record: WebNetworkCaptureRecord,
|
||||
): RecordedExchange | undefined => {
|
||||
@@ -185,14 +201,27 @@ export const parseViewEvidence = <Kind extends AnalysisViewResult["kind"]>(
|
||||
> => {
|
||||
const evidence = parseEvidenceFile(operation, role, value);
|
||||
if (!evidence.ok) return evidence;
|
||||
const view = analysisViewResultSchema.safeParse(
|
||||
evidence.value.normalized_result,
|
||||
const message = `The ${role} file is not ${kind} view Evidence.`;
|
||||
if (evidence.value.operation !== "inspect_analysis_view")
|
||||
return err(
|
||||
inputError(
|
||||
operation,
|
||||
[
|
||||
{
|
||||
path: [role, "operation"],
|
||||
reason: "invalid_value",
|
||||
expected: "inspect_analysis_view",
|
||||
message: `The ${role} file must be inspect_analysis_view Evidence of kind ${kind}.`,
|
||||
},
|
||||
],
|
||||
message,
|
||||
),
|
||||
);
|
||||
const normalized = immutableJsonSnapshotMember(
|
||||
evidence.value,
|
||||
"normalized_result",
|
||||
);
|
||||
if (
|
||||
evidence.value.operation !== "inspect_analysis_view" ||
|
||||
!view.success ||
|
||||
view.data.kind !== kind
|
||||
)
|
||||
if (immutableJsonSnapshotMember(normalized, "kind") !== kind)
|
||||
return err(
|
||||
inputError(
|
||||
operation,
|
||||
@@ -204,7 +233,21 @@ export const parseViewEvidence = <Kind extends AnalysisViewResult["kind"]>(
|
||||
message: `The ${role} file must be inspect_analysis_view Evidence of kind ${kind}.`,
|
||||
},
|
||||
],
|
||||
`The ${role} file is not ${kind} view Evidence.`,
|
||||
message,
|
||||
),
|
||||
);
|
||||
// The discriminator selected the requested view, so any failure is in that view's payload.
|
||||
const view = analysisViewResultSchema.safeParse(normalized);
|
||||
if (!view.success)
|
||||
return err(
|
||||
schemaInputError(
|
||||
operation,
|
||||
{
|
||||
prefix: [role, "normalized_result"],
|
||||
cause: view.error,
|
||||
value: normalized,
|
||||
},
|
||||
message,
|
||||
),
|
||||
);
|
||||
return ok({
|
||||
@@ -213,89 +256,132 @@ export const parseViewEvidence = <Kind extends AnalysisViewResult["kind"]>(
|
||||
});
|
||||
};
|
||||
|
||||
/** Authenticate the evidence file and require complete capture Evidence of either kind. */
|
||||
/** What replay needs from a parent capture once its payload has been validated. */
|
||||
interface CaptureFacts {
|
||||
readonly records: readonly WebNetworkCaptureRecord[];
|
||||
readonly captureKind: EnvironmentCaptureKind;
|
||||
readonly captureSha256: string | null;
|
||||
}
|
||||
|
||||
/** Issue paths of a capture payload refusal start here. */
|
||||
const CAPTURE_PAYLOAD_PATH = ["evidence", "normalized_result"] as const;
|
||||
|
||||
/** Validate a capture_browser_scenario payload; its failures name the payload fields. */
|
||||
const parseScenarioCapture = (
|
||||
operation: string,
|
||||
normalized: unknown,
|
||||
parameters: unknown,
|
||||
): Result<CaptureFacts, AnalysisError> => {
|
||||
const scenario = browserScenarioCaptureSchema.safeParse(normalized);
|
||||
if (!scenario.success)
|
||||
return err(
|
||||
schemaInputError(
|
||||
operation,
|
||||
{
|
||||
prefix: CAPTURE_PAYLOAD_PATH,
|
||||
cause: scenario.error,
|
||||
value: normalized,
|
||||
},
|
||||
"The evidence file is not a complete browser scenario capture.",
|
||||
),
|
||||
);
|
||||
const definition = z
|
||||
.object({ scenario_sha256: z.string() })
|
||||
.safeParse(parameters);
|
||||
return ok({
|
||||
records: scenarioCaptureRecords(scenario.data, "replay").records,
|
||||
captureKind: "browser-scenario",
|
||||
captureSha256: definition.success ? definition.data.scenario_sha256 : null,
|
||||
});
|
||||
};
|
||||
|
||||
/** Validate an inspect_web_network_capture payload as HAR; its failures name the payload fields. */
|
||||
const parseHarCapture = (
|
||||
operation: string,
|
||||
normalized: unknown,
|
||||
): Result<CaptureFacts, AnalysisError> => {
|
||||
const records = immutableJsonSnapshotMember(normalized, "records");
|
||||
if (Array.isArray(records))
|
||||
for (let index = 0; index < records.length; index += 1) {
|
||||
const record = immutableJsonSnapshotMember(records, String(index));
|
||||
immutableJsonSnapshotMember(record, "reported");
|
||||
}
|
||||
// An owned snapshot is validated once and borrowed, so deriving the state reuses this validation.
|
||||
const capture = safeParseOwnedWebNetworkCapture(normalized);
|
||||
if (!capture.success)
|
||||
return err(
|
||||
schemaInputError(
|
||||
operation,
|
||||
{
|
||||
prefix: CAPTURE_PAYLOAD_PATH,
|
||||
cause: capture.error,
|
||||
value: normalized,
|
||||
},
|
||||
"The evidence file is not a complete web network capture.",
|
||||
),
|
||||
);
|
||||
// The manifest view projects HAR captures only, and replay reads HAR's response fields.
|
||||
if (capture.data.format !== "har")
|
||||
return err(
|
||||
inputError(
|
||||
operation,
|
||||
[
|
||||
{
|
||||
path: [...CAPTURE_PAYLOAD_PATH, "format"],
|
||||
reason: "invalid_value",
|
||||
expected: "har",
|
||||
message: `Agent environments replay HAR captures; this capture's format is ${capture.data.format}.`,
|
||||
},
|
||||
],
|
||||
"The evidence file is not a HAR capture.",
|
||||
),
|
||||
);
|
||||
return ok({
|
||||
records: capture.data.records,
|
||||
captureKind: "web-network-capture",
|
||||
captureSha256: capture.data.artifact.sha256,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Authenticate the evidence file and require complete capture Evidence of
|
||||
* either kind. Only an operation no capture schema owns blames the operation;
|
||||
* a recognized operation's payload failures name the payload fields.
|
||||
*/
|
||||
const parseCaptureEvidence = (
|
||||
operation: string,
|
||||
value: unknown,
|
||||
): Result<
|
||||
{
|
||||
readonly records: readonly WebNetworkCaptureRecord[];
|
||||
readonly evidenceId: string;
|
||||
readonly captureKind: EnvironmentCaptureKind;
|
||||
readonly captureSha256: string | null;
|
||||
},
|
||||
AnalysisError
|
||||
> => {
|
||||
): Result<CaptureFacts & { readonly evidenceId: string }, AnalysisError> => {
|
||||
const evidence = parseEvidenceFile(operation, "evidence", value);
|
||||
if (!evidence.ok) return evidence;
|
||||
const { evidence_id: evidenceId } = evidence.value;
|
||||
const normalized = immutableJsonSnapshotMember(
|
||||
evidence.value,
|
||||
"normalized_result",
|
||||
);
|
||||
if (evidence.value.operation === "capture_browser_scenario") {
|
||||
const scenario = browserScenarioCaptureSchema.safeParse(normalized);
|
||||
const definition = z
|
||||
.object({ scenario_sha256: z.string() })
|
||||
.safeParse(evidence.value.parameters);
|
||||
if (scenario.success)
|
||||
return ok({
|
||||
records: scenarioCaptureRecords(scenario.data, "replay").records,
|
||||
evidenceId,
|
||||
captureKind: "browser-scenario",
|
||||
captureSha256: definition.success
|
||||
? definition.data.scenario_sha256
|
||||
: null,
|
||||
});
|
||||
} else if (evidence.value.operation === "inspect_web_network_capture") {
|
||||
const records = immutableJsonSnapshotMember(normalized, "records");
|
||||
if (Array.isArray(records))
|
||||
for (let index = 0; index < records.length; index += 1) {
|
||||
const record = immutableJsonSnapshotMember(records, String(index));
|
||||
immutableJsonSnapshotMember(record, "reported");
|
||||
}
|
||||
// An owned snapshot is validated once and borrowed, so deriving the state reuses this validation.
|
||||
const capture = safeParseOwnedWebNetworkCapture(normalized);
|
||||
if (capture.success) {
|
||||
// The manifest view projects HAR captures only, and replay reads HAR's response fields.
|
||||
if (capture.data.format !== "har")
|
||||
return err(
|
||||
inputError(
|
||||
operation,
|
||||
[
|
||||
{
|
||||
path: ["evidence", "normalized_result", "format"],
|
||||
reason: "invalid_value",
|
||||
expected: "har",
|
||||
message: `Agent environments replay HAR captures; this capture's format is ${capture.data.format}.`,
|
||||
},
|
||||
],
|
||||
"The evidence file is not a HAR capture.",
|
||||
),
|
||||
);
|
||||
return ok({
|
||||
records: capture.data.records,
|
||||
evidenceId,
|
||||
captureKind: "web-network-capture",
|
||||
captureSha256: capture.data.artifact.sha256,
|
||||
});
|
||||
}
|
||||
}
|
||||
return err(
|
||||
inputError(
|
||||
operation,
|
||||
[
|
||||
{
|
||||
path: ["evidence", "operation"],
|
||||
reason: "invalid_value",
|
||||
expected: "inspect_web_network_capture or capture_browser_scenario",
|
||||
message:
|
||||
"The evidence file must be complete inspect_web_network_capture or capture_browser_scenario Evidence.",
|
||||
},
|
||||
],
|
||||
"The evidence file is not web network capture or browser scenario Evidence.",
|
||||
),
|
||||
);
|
||||
const capture =
|
||||
evidence.value.operation === "capture_browser_scenario"
|
||||
? parseScenarioCapture(operation, normalized, evidence.value.parameters)
|
||||
: evidence.value.operation === "inspect_web_network_capture"
|
||||
? parseHarCapture(operation, normalized)
|
||||
: err(
|
||||
inputError(
|
||||
operation,
|
||||
[
|
||||
{
|
||||
path: ["evidence", "operation"],
|
||||
reason: "invalid_value",
|
||||
expected:
|
||||
"inspect_web_network_capture or capture_browser_scenario",
|
||||
message:
|
||||
"The evidence file must be inspect_web_network_capture or capture_browser_scenario Evidence.",
|
||||
},
|
||||
],
|
||||
"The evidence file is not web network capture or browser scenario Evidence.",
|
||||
),
|
||||
);
|
||||
return capture.ok
|
||||
? ok({ ...capture.value, evidenceId: evidence.value.evidence_id })
|
||||
: capture;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { metadataReportSubjectFormat } from "../../domain/apple/machoTargetSelection.js";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { GoBinaryPort } from "./GoBinaryPort.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
@@ -73,7 +74,7 @@ export class GoBinaryService {
|
||||
{
|
||||
path: value.artifact.path,
|
||||
sha256: value.artifact.sha256,
|
||||
format: value.format === "macho" ? "mach-o" : value.format,
|
||||
format: metadataReportSubjectFormat(value),
|
||||
...(architecture === undefined ? {} : { architecture }),
|
||||
},
|
||||
this.provider.identity,
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
import { chmod, mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect, it, vi } from "vitest";
|
||||
|
||||
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
import { electronActiveObservationInputSchema } from "../../domain/javascript/electronActiveObservation.js";
|
||||
import { err } from "../../domain/result.js";
|
||||
import { createTestTempDirectory } from "../../../tests/fixtures/temporaryDirectory.js";
|
||||
import type { ElectronActiveObservationPort } from "./ElectronActiveObservationPort.js";
|
||||
import { captureElectronScenario } from "./ElectronActiveObservationService.js";
|
||||
|
||||
const SKIP_PERMISSIONS =
|
||||
process.platform === "win32" || process.getuid?.() === 0;
|
||||
|
||||
/** A provider that records admitted input and never launches anything. */
|
||||
const recordingProvider = () => {
|
||||
const capture = vi.fn<ElectronActiveObservationPort["capture"]>(() =>
|
||||
Promise.resolve(
|
||||
err(
|
||||
new AnalysisCapabilityUnavailableError(
|
||||
"test",
|
||||
"capture_electron_scenario",
|
||||
"recording provider",
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
const provider: ElectronActiveObservationPort = {
|
||||
identity: () => ({ id: "test", name: "test", version: "1" }),
|
||||
capture,
|
||||
};
|
||||
return { provider, capture };
|
||||
};
|
||||
|
||||
const fixture = async () => {
|
||||
const root = await createTestTempDirectory("rea-electron-admission-");
|
||||
const denied = join(root, "denied");
|
||||
const readable = join(root, "readable");
|
||||
await mkdir(join(denied, "root"), { recursive: true });
|
||||
await mkdir(readable);
|
||||
const executable = join(denied, "electron");
|
||||
await writeFile(executable, "#!/bin/sh\n", { mode: 0o755 });
|
||||
await writeFile(join(denied, "main.js"), "module.exports = {};\n");
|
||||
await writeFile(join(readable, "main.js"), "module.exports = {};\n");
|
||||
return { root, denied, readable, executable };
|
||||
};
|
||||
|
||||
/** Remove search permission from `directory` for the duration of `run`. */
|
||||
const withoutSearchPermission = async <T>(
|
||||
directory: string,
|
||||
run: () => Promise<T>,
|
||||
): Promise<T> => {
|
||||
await chmod(directory, 0o000);
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
await chmod(directory, 0o700);
|
||||
}
|
||||
};
|
||||
|
||||
it
|
||||
.skipIf(SKIP_PERMISSIONS)
|
||||
.each(["application_path", "application_root", "executable_path"] as const)(
|
||||
"reports a search-denied %s as host access denial before any provider work",
|
||||
async (field) => {
|
||||
const { denied, readable, executable } = await fixture();
|
||||
const selected = {
|
||||
application_path: join(denied, "main.js"),
|
||||
application_root: join(denied, "root"),
|
||||
executable_path: executable,
|
||||
}[field];
|
||||
const { provider, capture } = recordingProvider();
|
||||
const result = await withoutSearchPermission(denied, () =>
|
||||
captureElectronScenario(
|
||||
provider,
|
||||
electronActiveObservationInputSchema.parse({
|
||||
executable_path: process.execPath,
|
||||
application_path: join(readable, "main.js"),
|
||||
actions: [],
|
||||
[field]: selected,
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "access_denied",
|
||||
category: "unavailable",
|
||||
details: {
|
||||
operation: "capture_electron_scenario",
|
||||
path: selected,
|
||||
system_code: "EACCES",
|
||||
boundary: "filesystem-read",
|
||||
},
|
||||
});
|
||||
expect(capture).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(SKIP_PERMISSIONS)(
|
||||
"reports invalid selections before a host denial on another path",
|
||||
async () => {
|
||||
const { root, denied } = await fixture();
|
||||
const missing = join(root, "missing.js");
|
||||
const { provider, capture } = recordingProvider();
|
||||
const result = await withoutSearchPermission(denied, () =>
|
||||
captureElectronScenario(
|
||||
provider,
|
||||
electronActiveObservationInputSchema.parse({
|
||||
executable_path: process.execPath,
|
||||
application_path: missing,
|
||||
application_root: join(denied, "root"),
|
||||
actions: [],
|
||||
}),
|
||||
),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
path: ["application_path"],
|
||||
reason: "invalid_value",
|
||||
message: expect.stringContaining("ENOENT"),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(capture).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(SKIP_PERMISSIONS).each([
|
||||
{ label: "0644", mode: 0o644 },
|
||||
{ label: "0000", mode: 0o000 },
|
||||
])(
|
||||
"keeps a reachable executable without execute permission (mode $label) as invalid input",
|
||||
async ({ mode }) => {
|
||||
const { root, readable } = await fixture();
|
||||
const executable = join(root, "not-executable");
|
||||
await writeFile(executable, "not executable");
|
||||
await chmod(executable, mode);
|
||||
const { provider, capture } = recordingProvider();
|
||||
const result = await captureElectronScenario(
|
||||
provider,
|
||||
electronActiveObservationInputSchema.parse({
|
||||
executable_path: executable,
|
||||
application_path: join(readable, "main.js"),
|
||||
actions: [],
|
||||
}),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
path: ["executable_path"],
|
||||
reason: "invalid_value",
|
||||
message: expect.stringContaining("is not an executable file"),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(capture).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("admits readable selections and hands canonical paths to the provider", async () => {
|
||||
const { readable } = await fixture();
|
||||
const { provider, capture } = recordingProvider();
|
||||
const application = join(readable, "main.js");
|
||||
await captureElectronScenario(
|
||||
provider,
|
||||
electronActiveObservationInputSchema.parse({
|
||||
executable_path: process.execPath,
|
||||
application_path: application,
|
||||
actions: [],
|
||||
}),
|
||||
);
|
||||
expect(capture).toHaveBeenCalledOnce();
|
||||
expect(capture.mock.calls[0]?.[0]).toMatchObject({
|
||||
application_path: application,
|
||||
application_root: readable,
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { dirname } from "node:path";
|
||||
import { constants } from "node:fs";
|
||||
import { constants, type Stats } from "node:fs";
|
||||
import { access, realpath, stat } from "node:fs/promises";
|
||||
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
ElectronActiveObservationPort,
|
||||
} from "./ElectronActiveObservationPort.js";
|
||||
import {
|
||||
AnalysisAccessDeniedError,
|
||||
AnalysisCapabilityUnavailableError,
|
||||
AnalysisInputError,
|
||||
type AnalysisInputIssue,
|
||||
@@ -21,53 +22,107 @@ import { createElectronActiveEvidence } from "./ElectronActiveEvidence.js";
|
||||
const OPERATION = "capture_electron_scenario" as const;
|
||||
const PROVIDER_ID = "rea-playwright-electron-active";
|
||||
|
||||
const resolveInputPath = async (
|
||||
field: "executable_path" | "application_path" | "application_root",
|
||||
type ElectronPathField =
|
||||
| "executable_path"
|
||||
| "application_path"
|
||||
| "application_root";
|
||||
|
||||
/**
|
||||
* Why a selected path was not admitted. Host permission denial is not
|
||||
* malformed caller input: the path may be correct, but REA cannot read it.
|
||||
*/
|
||||
type PathAdmissionFailure =
|
||||
| {
|
||||
readonly kind: "invalid";
|
||||
readonly issue: AnalysisInputIssue;
|
||||
readonly cause: unknown;
|
||||
}
|
||||
| { readonly kind: "denied"; readonly error: AnalysisAccessDeniedError };
|
||||
|
||||
const hostAccessDenial = (
|
||||
selected: string,
|
||||
): Promise<
|
||||
Result<
|
||||
string,
|
||||
{ readonly issue: AnalysisInputIssue; readonly cause: unknown }
|
||||
>
|
||||
> => {
|
||||
cause: unknown,
|
||||
): PathAdmissionFailure | undefined => {
|
||||
const code: unknown =
|
||||
cause instanceof Error ? Reflect.get(cause, "code") : undefined;
|
||||
return code === "EACCES" || code === "EPERM"
|
||||
? {
|
||||
kind: "denied",
|
||||
error: new AnalysisAccessDeniedError(OPERATION, selected, code, {
|
||||
cause,
|
||||
}),
|
||||
}
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const resolveInputPath = async (
|
||||
field: ElectronPathField,
|
||||
selected: string,
|
||||
): Promise<Result<string, PathAdmissionFailure>> => {
|
||||
try {
|
||||
return ok(await realpath(selected));
|
||||
} catch (cause: unknown) {
|
||||
return err({
|
||||
issue: {
|
||||
path: [field],
|
||||
reason: "invalid_value",
|
||||
message: `Cannot resolve ${field} ${selected}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
return err(
|
||||
hostAccessDenial(selected, cause) ?? {
|
||||
kind: "invalid",
|
||||
issue: {
|
||||
path: [field],
|
||||
reason: "invalid_value",
|
||||
message: `Cannot resolve ${field} ${selected}: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
},
|
||||
cause,
|
||||
},
|
||||
cause,
|
||||
});
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** Resolve the executable and require a launchable regular file before any spawn. */
|
||||
/**
|
||||
* Resolve the executable and require a launchable regular file before any
|
||||
* spawn. Denied resolution or metadata reads are host access failures; a
|
||||
* readable file without execute permission is an invalid selection.
|
||||
*/
|
||||
const resolveExecutablePath = async (
|
||||
selected: string,
|
||||
): ReturnType<typeof resolveInputPath> => {
|
||||
const resolved = await resolveInputPath("executable_path", selected);
|
||||
if (!resolved.ok) return resolved;
|
||||
let metadata: Stats;
|
||||
try {
|
||||
if (!(await stat(resolved.value)).isFile())
|
||||
throw new Error("not a regular file");
|
||||
if (process.platform !== "win32")
|
||||
await access(resolved.value, constants.X_OK);
|
||||
return resolved;
|
||||
metadata = await stat(resolved.value);
|
||||
} catch (cause: unknown) {
|
||||
return err({
|
||||
issue: {
|
||||
path: ["executable_path"],
|
||||
reason: "invalid_value",
|
||||
message: `executable_path ${selected} is not an executable file: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
},
|
||||
cause,
|
||||
});
|
||||
return err(
|
||||
hostAccessDenial(selected, cause) ?? notExecutable(selected, cause),
|
||||
);
|
||||
}
|
||||
if (!metadata.isFile())
|
||||
return err(notExecutable(selected, new Error("not a regular file")));
|
||||
if (process.platform !== "win32")
|
||||
try {
|
||||
await access(resolved.value, constants.X_OK);
|
||||
} catch (cause: unknown) {
|
||||
return err(notExecutable(selected, cause));
|
||||
}
|
||||
return resolved;
|
||||
};
|
||||
|
||||
const notExecutable = (
|
||||
selected: string,
|
||||
cause: unknown,
|
||||
): PathAdmissionFailure => ({
|
||||
kind: "invalid",
|
||||
issue: {
|
||||
path: ["executable_path"],
|
||||
reason: "invalid_value",
|
||||
message: `executable_path ${selected} is not an executable file: ${cause instanceof Error ? cause.message : String(cause)}`,
|
||||
},
|
||||
cause,
|
||||
});
|
||||
|
||||
/**
|
||||
* Admit every selected path before any provider work. Invalid selections are
|
||||
* reported together as input issues; a host access denial is reported only
|
||||
* when no selection is invalid, because correcting the request comes first.
|
||||
*/
|
||||
const canonicalizeInput = async (
|
||||
input: ElectronActiveObservationInput,
|
||||
): Promise<Result<CanonicalElectronActiveObservationInput, AnalysisError>> => {
|
||||
@@ -82,16 +137,23 @@ const canonicalizeInput = async (
|
||||
const failures = [executable, application, root].flatMap((result) =>
|
||||
result.ok ? [] : [result.error],
|
||||
);
|
||||
const issues = failures.flatMap((failure) =>
|
||||
failure.kind === "invalid" ? [failure.issue] : [],
|
||||
);
|
||||
const denial = failures.find((failure) => failure.kind === "denied");
|
||||
if (issues.length === 0 && denial !== undefined) return err(denial.error);
|
||||
return err(
|
||||
new AnalysisInputError(
|
||||
OPERATION,
|
||||
{
|
||||
cause: new AggregateError(
|
||||
failures.map(({ cause }) => cause),
|
||||
failures.map((failure) =>
|
||||
failure.kind === "invalid" ? failure.cause : failure.error,
|
||||
),
|
||||
"Electron paths could not be resolved",
|
||||
),
|
||||
},
|
||||
failures.map(({ issue }) => issue),
|
||||
issues,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
ASAR_HEADER_ADMISSION_SETTING,
|
||||
parseAsarHeaderAdmission,
|
||||
} from "../../config/asarHeaderAdmission.js";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
|
||||
@@ -7,10 +11,7 @@ import {
|
||||
type JavaScriptApplicationAnalysisResult,
|
||||
parseOwnedJavaScriptApplicationAnalysisSteps,
|
||||
} from "../../domain/javascript/javascriptApplicationAnalysis.js";
|
||||
import {
|
||||
AnalysisOutputError,
|
||||
AnalysisResourceConstraintError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
|
||||
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
@@ -32,11 +33,16 @@ import { JAVASCRIPT_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
|
||||
|
||||
const OPERATION = "analyze_javascript_application" as const;
|
||||
|
||||
/** Execution options plus the composition root's environment for admission settings. */
|
||||
export interface JavaScriptApplicationOptions extends ExecutionOptions {
|
||||
readonly environment?: Readonly<Record<string, string | undefined>>;
|
||||
}
|
||||
|
||||
/** Statically analyze one local JavaScript/Electron application. */
|
||||
export const analyzeJavaScriptApplication = async (
|
||||
rawInput: unknown,
|
||||
scope: ArtifactResourceScope,
|
||||
options: ExecutionOptions = {},
|
||||
options: JavaScriptApplicationOptions = {},
|
||||
): Promise<Result<Evidence, AnalysisError>> => {
|
||||
const parsed = analyzeJavaScriptApplicationInputSchema.safeParse(rawInput);
|
||||
if (!parsed.success)
|
||||
@@ -50,8 +56,13 @@ export const analyzeJavaScriptApplication = async (
|
||||
export const analyzeJavaScriptApplicationValidated = async (
|
||||
input: z.output<typeof analyzeJavaScriptApplicationInputSchema>,
|
||||
scope: ArtifactResourceScope,
|
||||
options: ExecutionOptions = {},
|
||||
options: JavaScriptApplicationOptions = {},
|
||||
): Promise<Result<Evidence, AnalysisError>> => {
|
||||
// An invalid setting is refused before analysis, never replaced by the default.
|
||||
const asarHeaderAdmission = parseAsarHeaderAdmission(
|
||||
options.environment?.[ASAR_HEADER_ADMISSION_SETTING],
|
||||
);
|
||||
if (!asarHeaderAdmission.ok) return asarHeaderAdmission;
|
||||
let reconstructed: JavaScriptArtifactReconstructionResult | undefined;
|
||||
let result: JavaScriptApplicationAnalysisResult | undefined;
|
||||
let evidence: Evidence | undefined;
|
||||
@@ -69,6 +80,7 @@ export const analyzeJavaScriptApplicationValidated = async (
|
||||
scope,
|
||||
options.signal,
|
||||
options.progress,
|
||||
asarHeaderAdmission.value,
|
||||
);
|
||||
const { electron_summary: summary, ...application } = reconstructed;
|
||||
await options.progress?.report({
|
||||
@@ -152,8 +164,13 @@ export const analyzeJavaScriptApplicationValidated = async (
|
||||
cause,
|
||||
}),
|
||||
);
|
||||
const filesystemFailure = hostFilesystemFailure(cause);
|
||||
if (filesystemFailure !== undefined) return err(filesystemFailure);
|
||||
if (
|
||||
cause instanceof Error &&
|
||||
"code" in cause &&
|
||||
typeof cause.code === "string" &&
|
||||
FILESYSTEM_ERROR_CODES.has(cause.code)
|
||||
)
|
||||
return err(new ArtifactOperationError(OPERATION, "io"));
|
||||
return err(
|
||||
new ProviderAdapterError(JAVASCRIPT_APPLICATION_PROVIDER.id, OPERATION, {
|
||||
cause,
|
||||
@@ -206,43 +223,6 @@ const describeResultSchemaFailure = (cause: z.ZodError): string => {
|
||||
const escapePointerPart = (part: string): string =>
|
||||
part.replaceAll("~", "~0").replaceAll("/", "~1");
|
||||
|
||||
/**
|
||||
* Project a host filesystem failure that no narrower owner classified. Selected
|
||||
* input denial is classified at selection; this failure may concern any path,
|
||||
* including private workspace files, so it keeps the host's own facts rather
|
||||
* than claiming a selected-read denial.
|
||||
*/
|
||||
const hostFilesystemFailure = (cause: unknown): AnalysisError | undefined => {
|
||||
if (!(cause instanceof Error)) return undefined;
|
||||
const code: unknown = Reflect.get(cause, "code");
|
||||
if (typeof code !== "string" || !FILESYSTEM_ERROR_CODES.has(code))
|
||||
return undefined;
|
||||
const syscall: unknown = Reflect.get(cause, "syscall");
|
||||
const path: unknown = Reflect.get(cause, "path");
|
||||
const facts = {
|
||||
system_code: code,
|
||||
...(typeof syscall === "string" ? { syscall } : {}),
|
||||
...(typeof path === "string" ? { path } : {}),
|
||||
};
|
||||
if (code === "EMFILE" || code === "ENFILE")
|
||||
return new AnalysisResourceConstraintError(
|
||||
OPERATION,
|
||||
"capacity",
|
||||
`Host file-descriptor limit reached (${code})${typeof path === "string" ? ` while accessing ${path}` : ""}`,
|
||||
facts,
|
||||
{
|
||||
cause,
|
||||
retryable: true,
|
||||
remediationAction:
|
||||
"Close unused files or raise the open-file limit for the process or host running REA, then retry.",
|
||||
},
|
||||
);
|
||||
return new ArtifactOperationError(OPERATION, "io", undefined, cause.message, {
|
||||
cause,
|
||||
diagnostics: facts,
|
||||
});
|
||||
};
|
||||
|
||||
const FILESYSTEM_ERROR_CODES = new Set([
|
||||
"ENOENT",
|
||||
"EACCES",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { constants, type Stats } from "node:fs";
|
||||
import {
|
||||
DEFAULT_ASAR_HEADER_ADMISSION,
|
||||
type AsarHeaderAdmission,
|
||||
} from "../../config/asarHeaderAdmission.js";
|
||||
import { constants } from "node:fs";
|
||||
import { access, lstat, realpath } from "node:fs/promises";
|
||||
import { setImmediate } from "node:timers/promises";
|
||||
|
||||
@@ -75,6 +79,7 @@ export const reconstructJavaScriptArtifact = async (
|
||||
scope: ArtifactResourceScope,
|
||||
signal?: AbortSignal,
|
||||
progress?: ProgressReporter,
|
||||
asarHeaderAdmission: AsarHeaderAdmission = DEFAULT_ASAR_HEADER_ADMISSION,
|
||||
): Promise<JavaScriptArtifactReconstructionResult> =>
|
||||
scope.run(async () => {
|
||||
const reportPhase = async (
|
||||
@@ -91,19 +96,20 @@ export const reconstructJavaScriptArtifact = async (
|
||||
};
|
||||
const input = javascriptArtifactReconstructionInputSchema.parse(rawInput);
|
||||
abortIfNeeded(signal);
|
||||
const { path, metadata } = await resolveSelectedInput(input.input_path);
|
||||
const format = selectedFormat(path, metadata, input);
|
||||
const path = await resolveSelectedInput(input.input_path);
|
||||
const format = await resolveFormat(path, input);
|
||||
const snapshot = await scanCanonicalArtifactInventoryInScope(path, {
|
||||
resourceScope: scope,
|
||||
signal,
|
||||
integrity: { mode: input.integrity_policy },
|
||||
asarHeaderAdmission,
|
||||
});
|
||||
if (snapshot.manifest.root_format !== format)
|
||||
throw new ArtifactReaderFailure(
|
||||
"format",
|
||||
`Artifact inventory classified ${path} as ${snapshot.manifest.root_format}, not ${format}`,
|
||||
);
|
||||
const reader = createReader(path, format);
|
||||
const reader = createReader(path, format, asarHeaderAdmission);
|
||||
const readerOwner = {
|
||||
kind: "reader" as const,
|
||||
reader,
|
||||
@@ -127,6 +133,7 @@ export const reconstructJavaScriptArtifact = async (
|
||||
scope,
|
||||
signal,
|
||||
input.source_paths,
|
||||
asarHeaderAdmission,
|
||||
);
|
||||
// Release acquisition before analysis consumes these owned file facts.
|
||||
const earlyCleanup = await scope.release(readerOwner);
|
||||
@@ -346,21 +353,13 @@ const OPERATION = "analyze_javascript_application";
|
||||
/**
|
||||
* Resolve the caller-selected input. A missing, looping, overlong, or
|
||||
* unreadable selection is a caller or host-resolution failure, not a damaged
|
||||
* artifact. A selected directory must also be searchable: without search
|
||||
* permission its entries can be listed but not inspected, so no entry is
|
||||
* readable. Other host failures propagate with their errno facts intact.
|
||||
* artifact.
|
||||
*/
|
||||
const resolveSelectedInput = async (
|
||||
inputPath: string,
|
||||
): Promise<{ readonly path: string; readonly metadata: Stats }> => {
|
||||
const resolveSelectedInput = async (inputPath: string): Promise<string> => {
|
||||
try {
|
||||
const path = await realpath(inputPath);
|
||||
const metadata = await lstat(path);
|
||||
await access(
|
||||
path,
|
||||
metadata.isDirectory() ? constants.R_OK | constants.X_OK : constants.R_OK,
|
||||
);
|
||||
return { path, metadata };
|
||||
await access(path, constants.R_OK);
|
||||
return path;
|
||||
} catch (cause: unknown) {
|
||||
const code =
|
||||
cause instanceof Error && "code" in cause ? String(cause.code) : "";
|
||||
@@ -397,11 +396,11 @@ const selectedInputResolutionMessage = (
|
||||
: `Selected input path could not be resolved because symbolic-link traversal looped or exceeded the host limit (ELOOP): ${inputPath}`;
|
||||
|
||||
/** The selected input's kind, checked against the caller's requested format. */
|
||||
const selectedFormat = (
|
||||
const resolveFormat = async (
|
||||
path: string,
|
||||
metadata: Stats,
|
||||
input: JavaScriptArtifactReconstructionInput,
|
||||
): "asar" | "directory" => {
|
||||
): Promise<"asar" | "directory"> => {
|
||||
const metadata = await lstat(path);
|
||||
const observed = metadata.isDirectory()
|
||||
? "directory"
|
||||
: metadata.isFile() && path.toLowerCase().endsWith(".asar")
|
||||
|
||||
@@ -20,12 +20,23 @@ export const NATIVE_DISPATCH_METADATA_SOURCES = {
|
||||
export const inspectNativeDispatch = async (
|
||||
analysis: AnalysisOperationPort,
|
||||
maxRecords: number,
|
||||
signal?: AbortSignal,
|
||||
objcClassImportAddresses?: readonly string[],
|
||||
objcClassImportAddressesComplete?: boolean,
|
||||
options: {
|
||||
readonly signal?: AbortSignal | undefined;
|
||||
readonly objcClassImportAddresses?: readonly string[] | undefined;
|
||||
readonly objcClassImportAddressesComplete?: boolean | undefined;
|
||||
/** Binary metadata input budget; symbol-only inventories ignore it. */
|
||||
readonly maxInputBytes?: number | undefined;
|
||||
} = {},
|
||||
): EnhancedResult => {
|
||||
const {
|
||||
signal,
|
||||
objcClassImportAddresses,
|
||||
objcClassImportAddressesComplete,
|
||||
maxInputBytes,
|
||||
} = options;
|
||||
const input = {
|
||||
max_records: maxRecords,
|
||||
...(maxInputBytes === undefined ? {} : { max_input_bytes: maxInputBytes }),
|
||||
...(objcClassImportAddresses === undefined
|
||||
? {}
|
||||
: {
|
||||
|
||||
@@ -46,7 +46,7 @@ export const traceNativeUiAction = async (
|
||||
},
|
||||
signal === undefined ? {} : { signal },
|
||||
),
|
||||
inspectNativeDispatch(analysis, 20_000, signal),
|
||||
inspectNativeDispatch(analysis, 20_000, { signal }),
|
||||
]);
|
||||
if (!uiExecution.ok && uiExecution.error._tag === "AnalysisCancelledError")
|
||||
return err(uiExecution.error);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { metadataReportSubjectFormat } from "../../domain/apple/machoTargetSelection.js";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { RustBinaryPort } from "./RustBinaryPort.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
@@ -9,6 +10,7 @@ import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
rustArchitectureForSubject,
|
||||
rustBinarySchema,
|
||||
type InspectRustBinaryInput,
|
||||
} from "../../domain/rust/rustBinary.js";
|
||||
import { inspectRustBinaryRequestSchema } from "../../domain/rust/rustBinaryQuery.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
@@ -82,7 +84,7 @@ export class RustBinaryService {
|
||||
|
||||
const evidenceFor = (
|
||||
provider: RustBinaryPort["identity"],
|
||||
parameters: { readonly path: string },
|
||||
parameters: InspectRustBinaryInput,
|
||||
value: ReturnType<typeof rustBinarySchema.parse>,
|
||||
): Evidence => {
|
||||
const architecture = rustArchitectureForSubject(value.architecture);
|
||||
@@ -91,7 +93,7 @@ const evidenceFor = (
|
||||
{
|
||||
path: value.artifact.path,
|
||||
sha256: value.artifact.sha256,
|
||||
format: value.format === "macho" ? "mach-o" : value.format,
|
||||
format: metadataReportSubjectFormat(value),
|
||||
...(architecture === undefined ? {} : { architecture }),
|
||||
},
|
||||
provider,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AsarHeaderAdmission } from "../config/asarHeaderAdmission.js";
|
||||
import {
|
||||
createAsarHeaderSignatureProbe,
|
||||
type FinalAsarHeaderProbeObservation,
|
||||
@@ -105,9 +106,11 @@ const finishClassificationEvidence = (
|
||||
};
|
||||
|
||||
/** Share acquisition bounds and signature observations across files and streams. */
|
||||
export const createArtifactClassificationProbe = () => {
|
||||
export const createArtifactClassificationProbe = (
|
||||
asarHeaderAdmission?: AsarHeaderAdmission,
|
||||
) => {
|
||||
const xml = createXmlPlistSignatureProbe();
|
||||
const asar = createAsarHeaderSignatureProbe();
|
||||
const asar = createAsarHeaderSignatureProbe(asarHeaderAdmission);
|
||||
const blocks: Buffer[] = [];
|
||||
let retainedBytes = 0;
|
||||
let observedBytes = 0;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AsarHeaderAdmission } from "../config/asarHeaderAdmission.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import type { Readable } from "node:stream";
|
||||
|
||||
@@ -30,10 +31,11 @@ export const hashReadable = async (
|
||||
stream: Readable,
|
||||
signal?: AbortSignal,
|
||||
integrityAlgorithm?: "sha256" | "md5",
|
||||
asarHeaderAdmission?: AsarHeaderAdmission,
|
||||
): Promise<HashResult> => {
|
||||
const hash = createHash("sha256");
|
||||
const md5 = integrityAlgorithm === "md5" ? createHash("md5") : undefined;
|
||||
const probe = createArtifactClassificationProbe();
|
||||
const probe = createArtifactClassificationProbe(asarHeaderAdmission);
|
||||
let bytes = 0;
|
||||
for await (const raw of stream) {
|
||||
abortIfNeeded(signal);
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
AnalysisUnsupportedTargetError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { ArtifactOperationError } from "../domain/artifactOperationError.js";
|
||||
import { BinaryTargetError } from "../domain/configurationErrors.js";
|
||||
import {
|
||||
BinaryTargetError,
|
||||
ConfigurationError,
|
||||
} from "../domain/configurationErrors.js";
|
||||
import { ArtifactReaderFailure } from "./ArtifactReader.js";
|
||||
import {
|
||||
artifactFileCancellationCause,
|
||||
@@ -40,13 +43,14 @@ export const translateArtifactFailure = (
|
||||
bindArtifactFileCleanup(cause, error);
|
||||
return error;
|
||||
}
|
||||
// Caller-selection, target and unsupported-target failures are already typed;
|
||||
// Caller-selection, target, unsupported-target and configuration failures are already typed;
|
||||
// keep their correction details instead of reducing them to an I/O failure.
|
||||
if (
|
||||
cause instanceof AnalysisInputError ||
|
||||
cause instanceof AnalysisCancelledError ||
|
||||
cause instanceof AnalysisUnsupportedTargetError ||
|
||||
cause instanceof BinaryTargetError
|
||||
cause instanceof BinaryTargetError ||
|
||||
cause instanceof ConfigurationError
|
||||
)
|
||||
return cause;
|
||||
return new ArtifactOperationError(operation, "io");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveAsarHeaderAdmission } from "./AsarHeader.js";
|
||||
import { artifactTargetRefusal } from "../domain/apple/artifactTargetApplicability.js";
|
||||
import { bindArtifactFileCleanup } from "./ArtifactFileOperation.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
@@ -480,6 +481,7 @@ class ArtifactClient implements AnalysisClient {
|
||||
return inventoryArtifact(this.target.sourcePath ?? this.target.path, {
|
||||
resourceScope: this.#resourceScope,
|
||||
environment: this.environment,
|
||||
asarHeaderAdmission: resolveAsarHeaderAdmission(this.environment),
|
||||
...(options?.signal === undefined ? {} : { signal: options.signal }),
|
||||
integrity: { mode: parsed.integrity_policy },
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AsarHeaderAdmission } from "../config/asarHeaderAdmission.js";
|
||||
import { artifactIntegrityDetails } from "../domain/artifactIntegrity.js";
|
||||
import { constants, createWriteStream, type Stats } from "node:fs";
|
||||
import {
|
||||
@@ -142,7 +143,11 @@ export class AsarArtifactReader implements ArtifactReader {
|
||||
try {
|
||||
admission = {
|
||||
ok: true,
|
||||
value: await admitAsarHeader(headerHandle, signal),
|
||||
value: await admitAsarHeader(
|
||||
headerHandle,
|
||||
signal,
|
||||
this.options.headerAdmission,
|
||||
),
|
||||
};
|
||||
} catch (cause: unknown) {
|
||||
admission = { ok: false, cause };
|
||||
@@ -622,7 +627,9 @@ type OwnedAsarHandle = {
|
||||
source?: Readable;
|
||||
output?: Readable;
|
||||
};
|
||||
type AsarArtifactReaderOptions = {
|
||||
export type AsarArtifactReaderOptions = {
|
||||
/** Header/index admission resolved by the caller's composition root; default otherwise. */
|
||||
readonly headerAdmission?: AsarHeaderAdmission;
|
||||
readonly openFile?: OpenAsarFile;
|
||||
readonly openUnpackedFile?: OpenAsarFile;
|
||||
readonly removeSnapshot?: (path: string) => Promise<void>;
|
||||
|
||||
+60
-27
@@ -4,40 +4,63 @@ import type { FileHandle } from "node:fs/promises";
|
||||
import { assertArtifactReadActive } from "./ArtifactFileOperation.js";
|
||||
import { ArtifactReaderFailure } from "./ArtifactReader.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import {
|
||||
ASAR_HEADER_ADMISSION_SETTING,
|
||||
DEFAULT_ASAR_HEADER_ADMISSION,
|
||||
DEFAULT_ASAR_MAX_HEADER_BYTES,
|
||||
MAXIMUM_ASAR_MAX_HEADER_BYTES,
|
||||
parseAsarHeaderAdmission,
|
||||
type AsarHeaderAdmission,
|
||||
} from "../config/asarHeaderAdmission.js";
|
||||
|
||||
/**
|
||||
* Fixed header/index admission for this release. The header is decoded before
|
||||
* any member path can be selected. There is no caller or configuration override.
|
||||
* Resolve the admission from a composition root's selected environment; an
|
||||
* invalid setting is refused, not ignored. Callers without an environment use
|
||||
* the default.
|
||||
*/
|
||||
export const MAX_ASAR_HEADER_BYTES = 16 * 1024 * 1024;
|
||||
export const resolveAsarHeaderAdmission = (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): AsarHeaderAdmission => {
|
||||
const parsed = parseAsarHeaderAdmission(
|
||||
environment[ASAR_HEADER_ADMISSION_SETTING],
|
||||
);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
return parsed.value;
|
||||
};
|
||||
|
||||
const MEBIBYTE = 1024 * 1024;
|
||||
|
||||
/** User-visible recovery for the fixed ASAR header/index admission budget. */
|
||||
export const asarHeaderAdmissionRecovery = (): string => {
|
||||
const bytes = MAX_ASAR_HEADER_BYTES;
|
||||
const byteAllowance = (bytes: number): string => {
|
||||
const mebibytes = bytes / MEBIBYTE;
|
||||
const allowance = Number.isInteger(mebibytes)
|
||||
return Number.isInteger(mebibytes)
|
||||
? `${String(mebibytes)} MiB / ${String(bytes)} bytes`
|
||||
: `${String(bytes)} bytes`;
|
||||
return [
|
||||
`ASAR header/index admission is limited to ${allowance}.`,
|
||||
"This release has no runtime override.",
|
||||
"Selecting fewer sources or narrowing an internal path does not bypass header admission, because the archive index is read before any member can be selected.",
|
||||
"Raising max_heap_mb or available host memory does not raise it.",
|
||||
"Analyze an extracted application directory, which does not admit an ASAR header, or supply an archive whose header fits this allowance.",
|
||||
"Repeating the unchanged request fails the same way.",
|
||||
].join(" ");
|
||||
};
|
||||
|
||||
/** Refuse an ASAR whose header/index exceeds the fixed admission budget. */
|
||||
export const asarHeaderAdmissionFailure = (): ArtifactReaderFailure =>
|
||||
new ArtifactReaderFailure("limit", asarHeaderAdmissionRecovery(), {
|
||||
admission: { kind: "asar-header", maximumBytes: MAX_ASAR_HEADER_BYTES },
|
||||
/** User-visible recovery for the effective ASAR header/index admission budget. */
|
||||
export const asarHeaderAdmissionRecovery = (
|
||||
admission: AsarHeaderAdmission,
|
||||
): string =>
|
||||
[
|
||||
`ASAR header/index admission is limited to ${byteAllowance(admission.maximumBytes)}${admission.source === "setting" ? ` by ${ASAR_HEADER_ADMISSION_SETTING}` : ""}.`,
|
||||
`Set ${ASAR_HEADER_ADMISSION_SETTING} in the environment that starts REA to a byte count up to ${byteAllowance(MAXIMUM_ASAR_MAX_HEADER_BYTES)} (default ${byteAllowance(DEFAULT_ASAR_MAX_HEADER_BYTES)}), then restart REA.`,
|
||||
"Selecting fewer sources or narrowing an internal path does not bypass header admission, because the archive index is read before any member can be selected.",
|
||||
"Raising max_heap_mb or available host memory does not raise it.",
|
||||
"Alternatively, analyze an extracted application directory, which does not admit an ASAR header.",
|
||||
"Repeating the unchanged request fails the same way.",
|
||||
].join(" ");
|
||||
|
||||
/** Refuse an ASAR whose header/index exceeds the effective admission budget. */
|
||||
export const asarHeaderAdmissionFailure = (
|
||||
admission: AsarHeaderAdmission,
|
||||
): ArtifactReaderFailure =>
|
||||
new ArtifactReaderFailure("limit", asarHeaderAdmissionRecovery(admission), {
|
||||
admission: { kind: "asar-header", ...admission },
|
||||
});
|
||||
|
||||
/** Absolute archive prefix required to inspect an admitted header. */
|
||||
export const MAX_ASAR_HEADER_PROBE_BYTES = MAX_ASAR_HEADER_BYTES + 8;
|
||||
/** Absolute archive prefix required to inspect a header under this admission. */
|
||||
export const asarHeaderProbeBytes = (admission: AsarHeaderAdmission): number =>
|
||||
admission.maximumBytes + 8;
|
||||
|
||||
export interface AsarHeaderExtents {
|
||||
readonly archiveBytes: number;
|
||||
@@ -77,6 +100,7 @@ export type AsarHeaderProbeObservation =
|
||||
readonly extents?: AsarHeaderProbeExtents;
|
||||
readonly scannedJsonBytes: number;
|
||||
readonly limitBytes: number;
|
||||
readonly admission: AsarHeaderAdmission;
|
||||
};
|
||||
|
||||
export type FinalAsarHeaderProbeObservation = Exclude<
|
||||
@@ -136,6 +160,7 @@ export const parseAsarHeaderExtents = (
|
||||
* scans the declared JSON bytes until the first non-JSON-whitespace byte.
|
||||
*/
|
||||
export interface AsarHeaderSignatureProbe {
|
||||
readonly admission: AsarHeaderAdmission;
|
||||
readonly limit: number;
|
||||
readonly observation: AsarHeaderProbeObservation;
|
||||
write(input: Uint8Array): AsarHeaderProbeObservation;
|
||||
@@ -143,13 +168,17 @@ export interface AsarHeaderSignatureProbe {
|
||||
}
|
||||
|
||||
class AsarHeaderSignatureProbeImpl implements AsarHeaderSignatureProbe {
|
||||
readonly limit = MAX_ASAR_HEADER_PROBE_BYTES;
|
||||
readonly limit: number;
|
||||
readonly #fixed = Buffer.alloc(16);
|
||||
#fixedBytes = 0;
|
||||
#scannedJsonBytes = 0;
|
||||
#extents: Omit<AsarHeaderExtents, "archiveBytes"> | undefined;
|
||||
#stop: ProbeStop = { state: "pending" };
|
||||
|
||||
constructor(readonly admission: AsarHeaderAdmission) {
|
||||
this.limit = asarHeaderProbeBytes(admission);
|
||||
}
|
||||
|
||||
get observation(): AsarHeaderProbeObservation {
|
||||
return this.#observation();
|
||||
}
|
||||
@@ -227,7 +256,7 @@ class AsarHeaderSignatureProbeImpl implements AsarHeaderSignatureProbe {
|
||||
) {
|
||||
this.#stop = { state: "rejected", reason: "archive-too-short" };
|
||||
} else if (
|
||||
this.#extents.headerBytes > MAX_ASAR_HEADER_BYTES &&
|
||||
this.#extents.headerBytes > this.admission.maximumBytes &&
|
||||
this.#stop.state === "matched"
|
||||
) {
|
||||
this.#stop = { state: "limit" };
|
||||
@@ -279,6 +308,7 @@ class AsarHeaderSignatureProbeImpl implements AsarHeaderSignatureProbe {
|
||||
...(extents === undefined ? {} : { extents }),
|
||||
scannedJsonBytes: this.#scannedJsonBytes,
|
||||
limitBytes: this.limit,
|
||||
admission: this.admission,
|
||||
};
|
||||
case "matched":
|
||||
return {
|
||||
@@ -308,6 +338,7 @@ class AsarHeaderSignatureProbeImpl implements AsarHeaderSignatureProbe {
|
||||
extents: finalizedExtents,
|
||||
scannedJsonBytes: this.#scannedJsonBytes,
|
||||
limitBytes: this.limit,
|
||||
admission: this.admission,
|
||||
};
|
||||
}
|
||||
return {
|
||||
@@ -346,6 +377,7 @@ const isJsonWhitespace = (value: number): boolean =>
|
||||
export const admitAsarHeader = async (
|
||||
handle: Pick<FileHandle, "read"> & { stat(): Promise<Stats> },
|
||||
signal?: AbortSignal,
|
||||
admission: AsarHeaderAdmission = DEFAULT_ASAR_HEADER_ADMISSION,
|
||||
): Promise<AsarHeaderExtents> => {
|
||||
assertArtifactReadActive(signal);
|
||||
const observed = await handle.stat();
|
||||
@@ -372,13 +404,14 @@ export const admitAsarHeader = async (
|
||||
}
|
||||
const parsed = parseAsarHeaderExtents(prefix, observed.size);
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
if (parsed.value.headerBytes > MAX_ASAR_HEADER_BYTES)
|
||||
throw asarHeaderAdmissionFailure();
|
||||
if (parsed.value.headerBytes > admission.maximumBytes)
|
||||
throw asarHeaderAdmissionFailure(admission);
|
||||
return {
|
||||
archiveBytes: observed.size,
|
||||
...parsed.value,
|
||||
};
|
||||
};
|
||||
|
||||
export const createAsarHeaderSignatureProbe = (): AsarHeaderSignatureProbe =>
|
||||
new AsarHeaderSignatureProbeImpl();
|
||||
export const createAsarHeaderSignatureProbe = (
|
||||
admission: AsarHeaderAdmission = DEFAULT_ASAR_HEADER_ADMISSION,
|
||||
): AsarHeaderSignatureProbe => new AsarHeaderSignatureProbeImpl(admission);
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveAsarHeaderAdmission } from "../AsarHeader.js";
|
||||
import { locateGodotPck } from "../GodotPckLocation.js";
|
||||
import { withArtifactRegularFile } from "../ArtifactFileOperation.js";
|
||||
import type { ArtifactCleanupAttempt } from "../ArtifactResourceScope.js";
|
||||
@@ -75,6 +76,7 @@ const extractArtifactInScope = async (
|
||||
resourceScope: input.resourceScope,
|
||||
signal,
|
||||
environment: input.environment,
|
||||
asarHeaderAdmission: resolveAsarHeaderAdmission(input.environment),
|
||||
integrity: { mode: input.integrityPolicy },
|
||||
});
|
||||
return materializeArtifactInventoryInScope(
|
||||
@@ -483,7 +485,10 @@ const createReader = async (
|
||||
return new PyInstallerArtifactReader(path);
|
||||
if (await requireExtractableFormat(path, input, signal))
|
||||
return new DirectoryArtifactReader(path);
|
||||
if (format === "asar") return new AsarArtifactReader(path);
|
||||
if (format === "asar")
|
||||
return new AsarArtifactReader(path, {
|
||||
headerAdmission: resolveAsarHeaderAdmission(input.environment),
|
||||
});
|
||||
if (isZipPackageFormat(format)) return new ZipArtifactReader(path, format);
|
||||
return new MachOSliceArtifactReader(path, input.environment);
|
||||
};
|
||||
|
||||
@@ -529,7 +529,8 @@ export const classifyArtifactContent = (
|
||||
path.toLowerCase().endsWith(".asar") &&
|
||||
asarHeader.state !== "rejected"
|
||||
) {
|
||||
if (asarHeader.state === "limit") throw asarHeaderAdmissionFailure();
|
||||
if (asarHeader.state === "limit")
|
||||
throw asarHeaderAdmissionFailure(asarHeader.admission);
|
||||
if (asarHeader.state === "matched")
|
||||
return { kind: "container", format: "asar" };
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js";
|
||||
import { locateGodotPck } from "../GodotPckLocation.js";
|
||||
import { sameRegularFileState } from "../../filesystem/RegularFile.js";
|
||||
import type { FileHandle } from "node:fs/promises";
|
||||
@@ -78,7 +79,10 @@ export const classifyAndHashRootForInventory = async (
|
||||
path: string,
|
||||
directory: boolean,
|
||||
expectedMetadata: Stats,
|
||||
options: Pick<ArtifactInventoryOptions, "resourceScope" | "signal">,
|
||||
options: Pick<
|
||||
ArtifactInventoryOptions,
|
||||
"resourceScope" | "signal" | "asarHeaderAdmission"
|
||||
>,
|
||||
): Promise<RootInventoryClassification> => {
|
||||
if (directory)
|
||||
return { format: "directory", digest: null, rootSource: undefined };
|
||||
@@ -106,7 +110,12 @@ export const classifyAndHashRootForInventory = async (
|
||||
"integrity",
|
||||
`Root artifact changed before inventory: ${path}`,
|
||||
);
|
||||
const format = await classifyRootFormat(path, handle, options.signal);
|
||||
const format = await classifyRootFormat(
|
||||
path,
|
||||
handle,
|
||||
options.signal,
|
||||
options.asarHeaderAdmission,
|
||||
);
|
||||
const digest = await hashStableRootArtifactHandle(
|
||||
path,
|
||||
handle,
|
||||
@@ -144,8 +153,9 @@ const classifyRootFormat = async (
|
||||
path: string,
|
||||
handle: FileHandle,
|
||||
signal?: AbortSignal,
|
||||
asarHeaderAdmission?: AsarHeaderAdmission,
|
||||
): Promise<ArtifactOccurrence["artifact_format"]> => {
|
||||
const probe = createArtifactClassificationProbe();
|
||||
const probe = createArtifactClassificationProbe(asarHeaderAdmission);
|
||||
const chunk = Buffer.alloc(64 * 1024);
|
||||
while (probe.length < probe.limit) {
|
||||
abortIfNeeded(signal);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js";
|
||||
import { AsarArtifactReader } from "../AsarArtifactReader.js";
|
||||
import type {
|
||||
ArtifactReader,
|
||||
@@ -24,6 +25,7 @@ export const createReader = (
|
||||
format: ArtifactOccurrence["artifact_format"],
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
rootSource?: StableRegularFileDescriptor,
|
||||
asarHeaderAdmission?: AsarHeaderAdmission,
|
||||
): ArtifactReader | undefined => {
|
||||
if (isZipPackageFormat(format))
|
||||
return new ZipArtifactReader(path, format, undefined, rootSource);
|
||||
@@ -31,7 +33,12 @@ export const createReader = (
|
||||
case "directory":
|
||||
return new DirectoryArtifactReader(path);
|
||||
case "asar":
|
||||
return new AsarArtifactReader(path);
|
||||
return new AsarArtifactReader(
|
||||
path,
|
||||
asarHeaderAdmission === undefined
|
||||
? {}
|
||||
: { headerAdmission: asarHeaderAdmission },
|
||||
);
|
||||
case "pyinstaller":
|
||||
return new PyInstallerArtifactReader(path, rootSource);
|
||||
case "godot-pck":
|
||||
|
||||
@@ -84,6 +84,7 @@ export const scanCanonicalArtifactInventoryInScope = async (
|
||||
rootFormat,
|
||||
options.environment ?? {},
|
||||
rootSource,
|
||||
options.asarHeaderAdmission,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
readerCreationFailure = { cause };
|
||||
@@ -101,7 +102,13 @@ export const scanCanonicalArtifactInventoryInScope = async (
|
||||
caseCollisionLimitation,
|
||||
unknownParentOccurrences,
|
||||
traversal,
|
||||
} = await scanReader(reader, ownedReaders, options.signal, integrity);
|
||||
} = await scanReader(
|
||||
reader,
|
||||
ownedReaders,
|
||||
options.signal,
|
||||
integrity,
|
||||
options.asarHeaderAdmission,
|
||||
);
|
||||
outcome = {
|
||||
kind: "completed",
|
||||
snapshot: await buildInventorySnapshot({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js";
|
||||
import {
|
||||
artifactIntegrityPair,
|
||||
artifactIntegrityDetails,
|
||||
@@ -56,6 +57,7 @@ interface ScanContext {
|
||||
readonly reader: ArtifactReader;
|
||||
readonly signal: AbortSignal | undefined;
|
||||
readonly integrity: ArtifactIntegrityPolicy;
|
||||
readonly asarHeaderAdmission: AsarHeaderAdmission | undefined;
|
||||
readonly nodes: Map<string, ArtifactNode>;
|
||||
readonly occurrences: MutableOccurrence[];
|
||||
readonly pendingContradictions: PendingIntegrityContradiction[];
|
||||
@@ -72,6 +74,7 @@ export const scanReader = async (
|
||||
ownedReaders: ArtifactReader[],
|
||||
signal?: AbortSignal,
|
||||
integrity: ArtifactIntegrityPolicy = STRICT_INTEGRITY_POLICY,
|
||||
asarHeaderAdmission?: AsarHeaderAdmission,
|
||||
): Promise<{
|
||||
readonly nodes: Map<string, ArtifactNode>;
|
||||
readonly occurrences: MutableOccurrence[];
|
||||
@@ -94,6 +97,7 @@ export const scanReader = async (
|
||||
reader,
|
||||
signal,
|
||||
integrity,
|
||||
asarHeaderAdmission,
|
||||
nodes,
|
||||
occurrences,
|
||||
pendingContradictions,
|
||||
@@ -248,7 +252,12 @@ const visitArtifactEntries = async (
|
||||
digested.classification.format === "asar" &&
|
||||
!digested.mismatched
|
||||
) {
|
||||
const nested = new AsarArtifactReader(entry.adapterKey);
|
||||
const nested = new AsarArtifactReader(
|
||||
entry.adapterKey,
|
||||
context.asarHeaderAdmission === undefined
|
||||
? {}
|
||||
: { headerAdmission: context.asarHeaderAdmission },
|
||||
);
|
||||
context.ownedReaders.push(nested);
|
||||
await nested.prepareContainer(digested.node.sha256, context.signal);
|
||||
// Only traversed containers can own members, not opaque ASAR-named files.
|
||||
@@ -304,6 +313,7 @@ const digestArtifactEntry = async (
|
||||
await currentReader.open(entry, context.signal),
|
||||
context.signal,
|
||||
entry.declaredIntegrity?.algorithm,
|
||||
context.asarHeaderAdmission,
|
||||
);
|
||||
const claim = entry.declaredIntegrity;
|
||||
const observed = claim?.algorithm === "md5" ? digest.md5 : digest.sha256;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js";
|
||||
import type { ArtifactResourceScope } from "../ArtifactResourceScope.js";
|
||||
|
||||
/** Resolved integrity behavior admitted to the artifact scanner. */
|
||||
@@ -15,4 +16,6 @@ export interface ArtifactInventoryOptions {
|
||||
readonly signal?: AbortSignal | undefined;
|
||||
readonly integrity?: ArtifactIntegrityPolicy | undefined;
|
||||
readonly environment?: Readonly<NodeJS.ProcessEnv> | undefined;
|
||||
/** ASAR header/index admission resolved from the owner's environment; default when absent. */
|
||||
readonly asarHeaderAdmission?: AsarHeaderAdmission | undefined;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js";
|
||||
import { createHash } from "node:crypto";
|
||||
import { isAbsolute } from "node:path";
|
||||
import type { Readable } from "node:stream";
|
||||
@@ -52,6 +53,7 @@ interface ReadContext {
|
||||
readonly files: JavaScriptArtifactFile[];
|
||||
readonly containers: JavaScriptArtifactContainer[];
|
||||
readonly signal: AbortSignal | undefined;
|
||||
readonly asarHeaderAdmission: AsarHeaderAdmission | undefined;
|
||||
readonly selectedPaths: ReadonlyMap<string, number> | undefined;
|
||||
textBytes: number;
|
||||
invalidUtf8: number;
|
||||
@@ -70,6 +72,7 @@ export const readJavaScriptArtifactFiles = async (
|
||||
resources: ArtifactResourceScope,
|
||||
signal?: AbortSignal,
|
||||
sourcePaths?: readonly string[],
|
||||
asarHeaderAdmission?: AsarHeaderAdmission,
|
||||
): Promise<JavaScriptArtifactFileSet> => {
|
||||
const inventory = expectedInventory(snapshot);
|
||||
const expected = inventory.files;
|
||||
@@ -129,6 +132,7 @@ export const readJavaScriptArtifactFiles = async (
|
||||
files: [],
|
||||
containers: [],
|
||||
signal,
|
||||
asarHeaderAdmission,
|
||||
selectedPaths,
|
||||
textBytes: 0,
|
||||
invalidUtf8: 0,
|
||||
@@ -250,7 +254,12 @@ const visitReader = async (
|
||||
context.signal,
|
||||
);
|
||||
context.containers.push(inventory);
|
||||
const nested = new AsarArtifactReader(entry.adapterKey);
|
||||
const nested = new AsarArtifactReader(
|
||||
entry.adapterKey,
|
||||
context.asarHeaderAdmission === undefined
|
||||
? {}
|
||||
: { headerAdmission: context.asarHeaderAdmission },
|
||||
);
|
||||
ownedReaders.push({ reader: nested, path });
|
||||
stack.push({
|
||||
reader: nested,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AsarHeaderAdmission } from "../../config/asarHeaderAdmission.js";
|
||||
import { AsarArtifactReader } from "../AsarArtifactReader.js";
|
||||
import type { ArtifactReader } from "../ArtifactReader.js";
|
||||
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
|
||||
@@ -6,7 +7,13 @@ import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
|
||||
export const createJavaScriptArtifactReader = (
|
||||
path: string,
|
||||
format: "asar" | "directory",
|
||||
asarHeaderAdmission?: AsarHeaderAdmission,
|
||||
): ArtifactReader =>
|
||||
format === "asar"
|
||||
? new AsarArtifactReader(path)
|
||||
? new AsarArtifactReader(
|
||||
path,
|
||||
asarHeaderAdmission === undefined
|
||||
? {}
|
||||
: { headerAdmission: asarHeaderAdmission },
|
||||
)
|
||||
: new DirectoryArtifactReader(path);
|
||||
@@ -386,8 +386,16 @@ const withTargetSession = async <
|
||||
let outcome: Result<Output, unknown>;
|
||||
let cleanupFailure: AnalysisError | undefined;
|
||||
try {
|
||||
const target = authorizeTarget(endpoint.discovery, input);
|
||||
const scopedInput = scopeToTargetOrigin(input, target);
|
||||
const { target, allowedOrigins } = authorizeCdpTarget(
|
||||
endpoint.discovery,
|
||||
input,
|
||||
operation,
|
||||
);
|
||||
// The authorization owner already derived the target origin scope.
|
||||
const scopedInput =
|
||||
input.allowed_origins.length > 0
|
||||
? input
|
||||
: { ...input, allowed_origins: [...allowedOrigins] };
|
||||
targetSession =
|
||||
endpoint.connection === undefined
|
||||
? await openCdpTargetSession(
|
||||
@@ -548,31 +556,13 @@ const selectTargets = (
|
||||
};
|
||||
};
|
||||
|
||||
const authorizeTarget = (
|
||||
discovery: CdpEndpointDiscovery,
|
||||
input: Pick<InspectWebPageInput, "target_id" | "allowed_origins">,
|
||||
): CdpEndpointTarget => {
|
||||
return authorizeCdpTarget(discovery, input, "inspect_web_page").target;
|
||||
};
|
||||
|
||||
const scopeToTargetOrigin = <T extends { allowed_origins: readonly string[] }>(
|
||||
input: T,
|
||||
target: CdpEndpointTarget,
|
||||
): T => {
|
||||
if (input.allowed_origins.length > 0) return input;
|
||||
const origin = sanitizeBrowserUrl(target.url).origin;
|
||||
if (origin === null)
|
||||
throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
|
||||
return { ...input, allowed_origins: [origin] };
|
||||
};
|
||||
|
||||
const providerError = (
|
||||
cause: unknown,
|
||||
operation: BrowserObservationOperation,
|
||||
signal?: AbortSignal,
|
||||
): AnalysisError =>
|
||||
cause instanceof BrowserObservationError && cause.operation !== operation
|
||||
? new BrowserObservationError(operation, cause.reason, { cause })
|
||||
cause instanceof BrowserObservationError
|
||||
? cause.forOperation(operation)
|
||||
: cause instanceof AnalysisError
|
||||
? cause
|
||||
: signal?.aborted === true
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DEFAULT_ASAR_HEADER_ADMISSION } from "../config/asarHeaderAdmission.js";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
@@ -54,6 +55,7 @@ describe("Electron frame capture", () => {
|
||||
},
|
||||
},
|
||||
completeness,
|
||||
DEFAULT_ASAR_HEADER_ADMISSION,
|
||||
);
|
||||
|
||||
expect(frames.map((frame) => frame.frame_id)).toEqual(["frame-main"]);
|
||||
@@ -133,6 +135,7 @@ describe("Electron frame capture", () => {
|
||||
scripts,
|
||||
frameIds: new Set(["frame-old", "frame-new"]),
|
||||
completeness,
|
||||
asarHeaderAdmission: DEFAULT_ASAR_HEADER_ADMISSION,
|
||||
});
|
||||
|
||||
expect(capture.total).toBe(4);
|
||||
@@ -212,6 +215,7 @@ it("admits a prefix and fitting suffix around an oversized Electron source", asy
|
||||
scripts,
|
||||
frameIds: new Set(),
|
||||
completeness,
|
||||
asarHeaderAdmission: DEFAULT_ASAR_HEADER_ADMISSION,
|
||||
});
|
||||
const byHash = new Map(
|
||||
capture.items.map((item) => [item.cdp_hash, item.source]),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AsarHeaderAdmission } from "../config/asarHeaderAdmission.js";
|
||||
import { digestCanonicalValue } from "../domain/canonicalDigest.js";
|
||||
import type { ProgressReporter } from "../application/ProgressReporter.js";
|
||||
import type {
|
||||
@@ -38,6 +39,8 @@ interface ElectronContext {
|
||||
readonly discovery: CdpEndpointDiscovery;
|
||||
readonly target: CdpEndpointTarget;
|
||||
readonly input: InspectElectronPageInput;
|
||||
/** Header admission resolved by the provider's composition root. */
|
||||
readonly asarHeaderAdmission: AsarHeaderAdmission;
|
||||
readonly signal?: AbortSignal;
|
||||
readonly progress?: ProgressReporter;
|
||||
}
|
||||
@@ -157,6 +160,7 @@ const authorizeInitialFrames = async (
|
||||
const frames = await captureFrames(
|
||||
result,
|
||||
state.completeness,
|
||||
context.asarHeaderAdmission,
|
||||
context.signal,
|
||||
);
|
||||
if (frames[0] === undefined)
|
||||
@@ -200,6 +204,7 @@ const captureElectronContent = async (
|
||||
scripts: state.scripts,
|
||||
frameIds,
|
||||
completeness: state.completeness,
|
||||
asarHeaderAdmission: context.asarHeaderAdmission,
|
||||
});
|
||||
const workers = await captureElectronWorkers({
|
||||
connection: context.connection,
|
||||
@@ -209,6 +214,7 @@ const captureElectronContent = async (
|
||||
frameIds,
|
||||
completeness: state.completeness,
|
||||
limitations: state.limitations,
|
||||
asarHeaderAdmission: context.asarHeaderAdmission,
|
||||
});
|
||||
return { dom, scripts, resources, workers };
|
||||
};
|
||||
@@ -224,7 +230,11 @@ const assertStableMainFrame = async (
|
||||
context.sessionId,
|
||||
context.signal,
|
||||
);
|
||||
const afterPath = await mainFilePath(result, context.signal);
|
||||
const afterPath = await mainFilePath(
|
||||
result,
|
||||
context.asarHeaderAdmission,
|
||||
context.signal,
|
||||
);
|
||||
if (afterPath === undefined)
|
||||
throw new BrowserObservationError("inspect_web_page", "target_not_allowed");
|
||||
if (state.navigationDuringCapture || afterPath !== expectedPath)
|
||||
@@ -244,6 +254,7 @@ const navigatedFrameId = (event: CdpEvent): string | undefined => {
|
||||
export const captureFrames = async (
|
||||
result: unknown,
|
||||
completeness: CdpCaptureCompleteness,
|
||||
asarHeaderAdmission: AsarHeaderAdmission,
|
||||
signal?: AbortSignal,
|
||||
): Promise<ElectronPageInspection["frames"]> => {
|
||||
const frameTree = recordValue(requiredRecord(result).frameTree);
|
||||
@@ -264,6 +275,7 @@ export const captureFrames = async (
|
||||
cdpStringValue(frame.url) ?? "",
|
||||
signal,
|
||||
"inspect_electron_page",
|
||||
asarHeaderAdmission,
|
||||
);
|
||||
if (path === undefined) {
|
||||
completeness.exclude("frames", "out_of_target_scope");
|
||||
@@ -301,6 +313,7 @@ const captureResources = async (
|
||||
cdpStringValue(resource.url) ?? "",
|
||||
context.signal,
|
||||
"inspect_electron_page",
|
||||
context.asarHeaderAdmission,
|
||||
);
|
||||
if (path === undefined) {
|
||||
completeness.exclude("resources", "out_of_target_scope");
|
||||
@@ -361,6 +374,7 @@ const captureDom = async (
|
||||
documentUrl,
|
||||
context.signal,
|
||||
"inspect_electron_page",
|
||||
context.asarHeaderAdmission,
|
||||
)) === undefined
|
||||
) {
|
||||
completeness.exclude("dom", "out_of_target_scope");
|
||||
@@ -399,6 +413,7 @@ const captureDom = async (
|
||||
|
||||
const mainFilePath = async (
|
||||
result: unknown,
|
||||
asarHeaderAdmission: AsarHeaderAdmission,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string | undefined> => {
|
||||
const frameTree = recordValue(requiredRecord(result).frameTree);
|
||||
@@ -407,6 +422,7 @@ const mainFilePath = async (
|
||||
cdpStringValue(frame?.url) ?? "",
|
||||
signal,
|
||||
"inspect_electron_page",
|
||||
asarHeaderAdmission,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
DEFAULT_ASAR_HEADER_ADMISSION,
|
||||
type AsarHeaderAdmission,
|
||||
} from "../config/asarHeaderAdmission.js";
|
||||
import type {
|
||||
ExecutionOptions,
|
||||
ProviderIdentity,
|
||||
@@ -46,6 +50,14 @@ import {
|
||||
/** Passive Electron provider for local file pages exposed by loopback CDP. */
|
||||
export class CdpElectronProvider implements ElectronObservationPort {
|
||||
readonly #owner = new CdpTargetSessionOwner();
|
||||
readonly #asarHeaderAdmission: AsarHeaderAdmission;
|
||||
|
||||
/** The admission comes from the composition root's environment; default otherwise. */
|
||||
constructor(
|
||||
asarHeaderAdmission: AsarHeaderAdmission = DEFAULT_ASAR_HEADER_ADMISSION,
|
||||
) {
|
||||
this.#asarHeaderAdmission = asarHeaderAdmission;
|
||||
}
|
||||
#activeOperations = 0;
|
||||
#closing = false;
|
||||
#closePromise: Promise<void> | undefined;
|
||||
@@ -148,6 +160,7 @@ export class CdpElectronProvider implements ElectronObservationPort {
|
||||
target.url,
|
||||
options.signal,
|
||||
"list_electron_targets",
|
||||
this.#asarHeaderAdmission,
|
||||
);
|
||||
if (path === undefined) {
|
||||
examined += 1;
|
||||
@@ -271,6 +284,7 @@ export class CdpElectronProvider implements ElectronObservationPort {
|
||||
const target = await authorizeTarget(
|
||||
discovery.targets,
|
||||
input.target_id,
|
||||
this.#asarHeaderAdmission,
|
||||
options.signal,
|
||||
);
|
||||
targetSession = await openCdpTargetSession(
|
||||
@@ -293,6 +307,7 @@ export class CdpElectronProvider implements ElectronObservationPort {
|
||||
discovery,
|
||||
target,
|
||||
input,
|
||||
asarHeaderAdmission: this.#asarHeaderAdmission,
|
||||
...(options.signal === undefined ? {} : { signal: options.signal }),
|
||||
...(options.progress === undefined
|
||||
? {}
|
||||
@@ -417,6 +432,7 @@ export class CdpElectronProvider implements ElectronObservationPort {
|
||||
const authorizeTarget = async (
|
||||
targets: readonly CdpEndpointTarget[],
|
||||
targetId: string,
|
||||
asarHeaderAdmission: AsarHeaderAdmission,
|
||||
signal?: AbortSignal,
|
||||
): Promise<CdpEndpointTarget> => {
|
||||
throwIfElectronOperationCancelled(signal, "inspect_electron_page");
|
||||
@@ -429,6 +445,7 @@ const authorizeTarget = async (
|
||||
target.url,
|
||||
signal,
|
||||
"inspect_electron_page",
|
||||
asarHeaderAdmission,
|
||||
);
|
||||
throwIfElectronOperationCancelled(signal, "inspect_electron_page");
|
||||
if (path === undefined)
|
||||
@@ -454,9 +471,8 @@ const providerError = (
|
||||
operation,
|
||||
);
|
||||
if (fileFailure !== undefined) return fileFailure;
|
||||
return cause instanceof BrowserObservationError &&
|
||||
cause.operation !== operation
|
||||
? new BrowserObservationError(operation, cause.reason, { cause })
|
||||
return cause instanceof BrowserObservationError
|
||||
? cause.forOperation(operation)
|
||||
: cause instanceof AnalysisError
|
||||
? cause
|
||||
: new ProviderAdapterError(CDP_ELECTRON_PROVIDER_IDENTITY.id, operation, {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AsarHeaderAdmission } from "../config/asarHeaderAdmission.js";
|
||||
import type {
|
||||
ElectronPageInspection,
|
||||
InspectElectronPageInput,
|
||||
@@ -26,6 +27,7 @@ interface ElectronScriptCaptureInput {
|
||||
readonly scripts: readonly ElectronScriptDraft[];
|
||||
readonly frameIds: ReadonlySet<string>;
|
||||
readonly completeness: CdpCaptureCompleteness;
|
||||
readonly asarHeaderAdmission: AsarHeaderAdmission;
|
||||
}
|
||||
|
||||
/** Normalize local script metadata and optionally approved source. */
|
||||
@@ -51,6 +53,7 @@ export const captureElectronScripts = async (
|
||||
script.rawUrl,
|
||||
input.signal,
|
||||
"inspect_electron_page",
|
||||
input.asarHeaderAdmission,
|
||||
);
|
||||
if (path === undefined) {
|
||||
input.completeness.exclude("scripts", "out_of_target_scope");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { AsarHeaderAdmission } from "../config/asarHeaderAdmission.js";
|
||||
import type {
|
||||
ElectronPageInspection,
|
||||
InspectElectronPageInput,
|
||||
@@ -20,6 +21,7 @@ export const captureElectronWorkers = async (input: {
|
||||
readonly frameIds: ReadonlySet<string>;
|
||||
readonly completeness: CdpCaptureCompleteness;
|
||||
readonly limitations: string[];
|
||||
readonly asarHeaderAdmission: AsarHeaderAdmission;
|
||||
}): Promise<ElectronPageInspection["workers"]> => {
|
||||
const result = await optionalCdpCommand(
|
||||
{
|
||||
@@ -64,6 +66,7 @@ export const captureElectronWorkers = async (input: {
|
||||
cdpStringValue(target.url) ?? "",
|
||||
input.signal,
|
||||
"inspect_electron_page",
|
||||
input.asarHeaderAdmission,
|
||||
);
|
||||
if (path === undefined) {
|
||||
input.completeness.exclude("workers", "out_of_target_scope");
|
||||
|
||||
@@ -19,8 +19,12 @@ import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
|
||||
import {
|
||||
admitAsarHeader,
|
||||
MAX_ASAR_HEADER_BYTES,
|
||||
asarHeaderAdmissionFailure,
|
||||
} from "../artifacts/AsarHeader.js";
|
||||
import {
|
||||
DEFAULT_ASAR_HEADER_ADMISSION,
|
||||
type AsarHeaderAdmission,
|
||||
} from "../config/asarHeaderAdmission.js";
|
||||
|
||||
type ElectronFileOperation =
|
||||
| "inspect_electron_page"
|
||||
@@ -37,10 +41,12 @@ export const authorizedElectronFile = async (
|
||||
value: string,
|
||||
signal?: AbortSignal,
|
||||
operation: ElectronFileOperation = "inspect_electron_page",
|
||||
asarHeaderAdmission: AsarHeaderAdmission = DEFAULT_ASAR_HEADER_ADMISSION,
|
||||
): Promise<string | undefined> => {
|
||||
try {
|
||||
return await resources.run(
|
||||
async () => await resolveAuthorizedElectronFile(value, signal),
|
||||
async () =>
|
||||
await resolveAuthorizedElectronFile(value, signal, asarHeaderAdmission),
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof ArtifactReaderFailure && cause.cleanup !== undefined)
|
||||
@@ -110,7 +116,8 @@ export const mapElectronFileAuthorizationFailure = (
|
||||
|
||||
const resolveAuthorizedElectronFile = async (
|
||||
value: string,
|
||||
signal?: AbortSignal,
|
||||
signal: AbortSignal | undefined,
|
||||
asarHeaderAdmission: AsarHeaderAdmission,
|
||||
): Promise<string | undefined> => {
|
||||
throwIfAborted(signal);
|
||||
if (/%(?:2f|5c)/iu.test(value)) return undefined;
|
||||
@@ -153,7 +160,7 @@ const resolveAuthorizedElectronFile = async (
|
||||
void cause;
|
||||
}
|
||||
throwIfAborted(signal);
|
||||
return await authorizedAsarMember(path, signal);
|
||||
return await authorizedAsarMember(path, signal, asarHeaderAdmission);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -163,7 +170,8 @@ const resolveAuthorizedElectronFile = async (
|
||||
*/
|
||||
const authorizedAsarMember = async (
|
||||
path: string,
|
||||
signal?: AbortSignal,
|
||||
signal: AbortSignal | undefined,
|
||||
asarHeaderAdmission: AsarHeaderAdmission,
|
||||
): Promise<string | undefined> => {
|
||||
const segments = path.split(sep);
|
||||
for (let index = 1; index < segments.length - 1; index += 1) {
|
||||
@@ -179,7 +187,13 @@ const authorizedAsarMember = async (
|
||||
if (!metadata.isFile()) continue;
|
||||
const canonical = await realpath(archive);
|
||||
throwIfAborted(signal);
|
||||
const files = await asarFiles(canonical, metadata, signal, resources);
|
||||
const files = await asarFiles(
|
||||
canonical,
|
||||
metadata,
|
||||
asarHeaderAdmission,
|
||||
signal,
|
||||
resources,
|
||||
);
|
||||
throwIfAborted(signal);
|
||||
return isAsarRegularFile(files, member)
|
||||
? join(canonical, ...member)
|
||||
@@ -207,6 +221,7 @@ const headerCache = new Map<
|
||||
readonly identity: string;
|
||||
readonly files: unknown;
|
||||
readonly bytes: number;
|
||||
readonly headerBytes: number;
|
||||
}
|
||||
>();
|
||||
const archiveIdentity = (metadata: ArchiveIdentity): string =>
|
||||
@@ -216,13 +231,19 @@ const archiveIdentity = (metadata: ArchiveIdentity): string =>
|
||||
const asarFiles = async (
|
||||
archive: string,
|
||||
metadata: ArchiveIdentity,
|
||||
admission: AsarHeaderAdmission,
|
||||
signal?: AbortSignal,
|
||||
resources?: ArtifactResourceScope,
|
||||
): Promise<unknown> => {
|
||||
throwIfAborted(signal);
|
||||
const identity = archiveIdentity(metadata);
|
||||
const cached = headerCache.get(archive);
|
||||
if (cached?.identity === identity) return cached.files;
|
||||
if (cached?.identity === identity) {
|
||||
// A header cached under a larger caller's admission stays refused here.
|
||||
if (cached.headerBytes > admission.maximumBytes)
|
||||
throw asarHeaderAdmissionFailure(admission);
|
||||
return cached.files;
|
||||
}
|
||||
const handle = await open(archive, "r");
|
||||
const admitted = await withArtifactFileHandle(
|
||||
archive,
|
||||
@@ -231,14 +252,16 @@ const asarFiles = async (
|
||||
signal,
|
||||
async (file) => {
|
||||
let jsonBytes: number;
|
||||
let headerBytes: number;
|
||||
throwIfAborted(signal);
|
||||
const opened = await file.stat();
|
||||
throwIfAborted(signal);
|
||||
if (!opened.isFile() || archiveIdentity(opened) !== identity)
|
||||
throw new Error("ASAR identity changed before header read");
|
||||
const header = await admitAsarHeader(file, signal);
|
||||
const header = await admitAsarHeader(file, signal, admission);
|
||||
throwIfAborted(signal);
|
||||
jsonBytes = header.jsonBytes;
|
||||
headerBytes = header.headerBytes;
|
||||
const json = Buffer.alloc(jsonBytes);
|
||||
let offset = 0;
|
||||
while (offset < json.length) {
|
||||
@@ -262,7 +285,7 @@ const asarFiles = async (
|
||||
);
|
||||
if (!isRecord(parsed) || !isRecord(parsed.files))
|
||||
throw new Error("ASAR header has no file tree");
|
||||
return { files: parsed.files, jsonBytes };
|
||||
return { files: parsed.files, jsonBytes, headerBytes };
|
||||
},
|
||||
);
|
||||
throwIfAborted(signal);
|
||||
@@ -271,12 +294,13 @@ const asarFiles = async (
|
||||
identity,
|
||||
files: admitted.files,
|
||||
bytes: admitted.jsonBytes,
|
||||
headerBytes: admitted.headerBytes,
|
||||
});
|
||||
const retainedBytes = () =>
|
||||
[...headerCache.values()].reduce((sum, entry) => sum + entry.bytes, 0);
|
||||
while (
|
||||
headerCache.size > MAX_CACHED_HEADERS ||
|
||||
retainedBytes() > MAX_ASAR_HEADER_BYTES
|
||||
retainedBytes() > admission.maximumBytes
|
||||
)
|
||||
headerCache.delete(headerCache.keys().next().value ?? archive);
|
||||
return admitted.files;
|
||||
|
||||
@@ -33,7 +33,7 @@ const hookSnapshotSchema = z.strictObject({
|
||||
dropped_runtime: retention.dropped_runtime,
|
||||
dropped_event_families: retention.dropped_event_families,
|
||||
dropped_event_roles: retention.dropped_event_roles,
|
||||
observed: z.number().int().min(0),
|
||||
observed: z.int().min(0),
|
||||
observed_ipc: retention.observed_ipc,
|
||||
observed_runtime: retention.observed_runtime,
|
||||
hook_error: z.boolean(),
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import type { BrowserScenarioSecrets } from "./BrowserScenarioSecrets.js";
|
||||
|
||||
const historyValueSchema = z.strictObject({
|
||||
length: z.number().int().min(0),
|
||||
length: z.int().min(0),
|
||||
navigation_entries: z.array(
|
||||
z.strictObject({
|
||||
type: z.string(),
|
||||
|
||||
@@ -24,10 +24,10 @@ export const captureWebEventListeners = async (
|
||||
await session.enable(domain);
|
||||
await session.assertDocument();
|
||||
const document = z
|
||||
.object({ root: z.object({ nodeId: z.number().int().min(1) }) })
|
||||
.object({ root: z.object({ nodeId: z.int().min(1) }) })
|
||||
.parse(await session.command("DOM.getDocument", { depth: 0 }));
|
||||
const selected = z
|
||||
.object({ nodeId: z.number().int().min(0) })
|
||||
.object({ nodeId: z.int().min(0) })
|
||||
.parse(
|
||||
await queryListenerSelector(
|
||||
session,
|
||||
@@ -39,7 +39,7 @@ export const captureWebEventListeners = async (
|
||||
let descriptors: z.infer<typeof runtimeListenersSchema>["listeners"] = [];
|
||||
if (selected.nodeId !== 0) {
|
||||
const described = z
|
||||
.object({ node: z.object({ backendNodeId: z.number().int().min(1) }) })
|
||||
.object({ node: z.object({ backendNodeId: z.int().min(1) }) })
|
||||
.parse(
|
||||
await session.command("DOM.describeNode", {
|
||||
nodeId: selected.nodeId,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const integer = z.number().int().min(0);
|
||||
const integer = z.int().min(0);
|
||||
const contextAuxDataSchema = z
|
||||
.object({
|
||||
frameId: z.string().optional(),
|
||||
|
||||
@@ -60,6 +60,7 @@ import {
|
||||
OwnedCommandFailure,
|
||||
} from "../process/OwnedCommand.js";
|
||||
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
|
||||
import { privateWorkspaceFailure } from "../process/privateWorkspaceFailure.js";
|
||||
|
||||
const OPERATION = "inspect_packet_capture";
|
||||
export const TSHARK_PROVIDER_IDENTITY = {
|
||||
@@ -498,32 +499,13 @@ export class TsharkCaptureProvider implements PacketCapturePort {
|
||||
effectiveLimits,
|
||||
operation,
|
||||
);
|
||||
if (phase === "workspace") {
|
||||
const code =
|
||||
cause instanceof Error &&
|
||||
"code" in cause &&
|
||||
typeof cause.code === "string"
|
||||
? cause.code
|
||||
: null;
|
||||
if (code === "ENOSPC" || code === "EDQUOT" || code === "EFBIG")
|
||||
return new AnalysisResourceConstraintError(
|
||||
operation,
|
||||
"file-size",
|
||||
`Private analysis workspace failed (${code}): ${reason}`,
|
||||
{ workspace_path: workspacePath, system_code: code },
|
||||
{ cause },
|
||||
);
|
||||
return new ProviderAdapterError(TSHARK_PROVIDER_IDENTITY.id, operation, {
|
||||
if (phase === "workspace")
|
||||
return privateWorkspaceFailure(
|
||||
TSHARK_PROVIDER_IDENTITY.id,
|
||||
operation,
|
||||
cause,
|
||||
userMessage: `Cannot prepare the private analysis workspace: ${reason}`,
|
||||
diagnostics: {
|
||||
stage: "workspace",
|
||||
workspace_path: workspacePath,
|
||||
system_code: code,
|
||||
reason,
|
||||
},
|
||||
});
|
||||
}
|
||||
workspacePath,
|
||||
);
|
||||
if (phase === "integrity") {
|
||||
const detail = {
|
||||
cause,
|
||||
|
||||
+7
-2
@@ -121,7 +121,12 @@ export const createCli = (
|
||||
registerSetupCommands(cli, logger, environment);
|
||||
registerReverseEngineeringCommands(cli, logger, environment);
|
||||
registerCutterCommands(cli, logger, environment);
|
||||
registerCoreAnalysisCommands(cli, logger, analysis.runDirectAnalysis);
|
||||
registerCoreAnalysisCommands(
|
||||
cli,
|
||||
logger,
|
||||
analysis.runDirectAnalysis,
|
||||
environment,
|
||||
);
|
||||
registerUtilityCommands(cli, logger, environment, analysis);
|
||||
registerWasmCommands(cli, logger, environment);
|
||||
registerCaptureCommands(cli, logger, environment);
|
||||
@@ -160,7 +165,7 @@ export const createCli = (
|
||||
registerAdvancedBrowserCommands(cli, logger);
|
||||
registerBrowserScenarioCommands(cli, logger, environment);
|
||||
registerElectronCommands(cli, logger, environment);
|
||||
registerJavaScriptRuntimeObservationCommands(cli, logger);
|
||||
registerJavaScriptRuntimeObservationCommands(cli, logger, environment);
|
||||
registerApplicationCommands(cli, logger);
|
||||
presentOmittableDefaults(cli);
|
||||
if (resultOutput !== undefined) streamCliCommandResults(cli, resultOutput);
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
createRecordedCrashService,
|
||||
createBinaryLayoutService,
|
||||
createPeResourcesService,
|
||||
createPeCertificatesService,
|
||||
} from "../composition/binaryDiagnostics.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
@@ -12,6 +13,7 @@ import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { inspectBinaryLayoutInputSchema } from "../domain/native/binaryLayout.js";
|
||||
import { inspectRecordedCrashInputSchema } from "../domain/native/recordedCrash.js";
|
||||
import { inspectPeResourcesInputSchema } from "../domain/native/peResources.js";
|
||||
import { inspectPeCertificatesInputSchema } from "../domain/native/peCertificates.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
@@ -22,6 +24,39 @@ export const registerBinaryDiagnosticsCommands = (
|
||||
logger: Logger,
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectPeCertificates, {
|
||||
description:
|
||||
"Inspect PE Authenticode certificate table identities without verifying signatures",
|
||||
args: z.object({ path: z.string().describe("Local PE32/PE32+ image") }),
|
||||
options: z.object({
|
||||
maxFileBytes: inspectPeCertificatesInputSchema.shape.max_file_bytes,
|
||||
maxEntries: inspectPeCertificatesInputSchema.shape.max_entries,
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation(async (signal) => {
|
||||
const pe = createPeCertificatesService();
|
||||
return await logCliCommand(
|
||||
logger,
|
||||
CLI_COMMANDS.inspectPeCertificates,
|
||||
async () =>
|
||||
finishAnalysisCommand(
|
||||
() =>
|
||||
pe.inspect(
|
||||
{
|
||||
path: absoluteCallerPath(args.path),
|
||||
max_file_bytes: options.maxFileBytes,
|
||||
max_entries: options.maxEntries,
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
pe,
|
||||
"rea-pe-certificates",
|
||||
"inspect_pe_certificates",
|
||||
signal,
|
||||
),
|
||||
);
|
||||
}),
|
||||
});
|
||||
cli.command(CLI_COMMANDS.inspectPeResources, {
|
||||
description:
|
||||
"Inspect PE resource identities, payload ranges, hashes and icon-group relationships",
|
||||
|
||||
@@ -9,7 +9,8 @@ export const registerCoreAnalysisCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): void => {
|
||||
registerCoreBinaryCommands(cli, logger, runDirectAnalysis);
|
||||
registerCoreBinaryCommands(cli, logger, runDirectAnalysis, environment);
|
||||
registerCoreNativeCommands(cli, logger, runDirectAnalysis);
|
||||
};
|
||||
@@ -23,8 +23,9 @@ export const registerCoreBinaryCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): void => {
|
||||
registerOverviewCommands(cli, logger, runDirectAnalysis);
|
||||
registerOverviewCommands(cli, logger, runDirectAnalysis, environment);
|
||||
registerDecompileCommand(cli, logger, runDirectAnalysis);
|
||||
registerFunctionCommand(cli, logger, runDirectAnalysis);
|
||||
registerInstructionsCommand(cli, logger, runDirectAnalysis);
|
||||
@@ -37,6 +38,7 @@ const registerOverviewCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): void => {
|
||||
const overviewOptions = z.object({
|
||||
snapshot: snapshotSelectionOption,
|
||||
@@ -88,6 +90,7 @@ const registerOverviewCommands = (
|
||||
? {}
|
||||
: { source_paths: options.sourcePath }),
|
||||
},
|
||||
environment,
|
||||
signal,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
|
||||
import { z } from "incur";
|
||||
|
||||
import type { DirectAnalysis } from "../composition/directAnalysis.js";
|
||||
@@ -272,6 +273,9 @@ const registerNativeDispatchMetadataCommand = (
|
||||
.max(20_000)
|
||||
.default(5_000)
|
||||
.describe("Maximum symbol records to inspect"),
|
||||
maxInputBytes:
|
||||
enhancedInputSchemas.inspect_native_dispatch_metadata.shape
|
||||
.max_input_bytes,
|
||||
snapshot: snapshotSelectionOption.describe(
|
||||
"Load or update the local analysis snapshot",
|
||||
),
|
||||
@@ -282,7 +286,12 @@ const registerNativeDispatchMetadataCommand = (
|
||||
runDirectAnalysis(
|
||||
args.path,
|
||||
"inspect_native_dispatch_metadata",
|
||||
{ max_records: options.maxRecords },
|
||||
{
|
||||
max_records: options.maxRecords,
|
||||
...(options.maxInputBytes === undefined
|
||||
? {}
|
||||
: { max_input_bytes: options.maxInputBytes }),
|
||||
},
|
||||
directAnalysisOptionsFromCli(logger, options),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -38,10 +38,10 @@ export const registerElectronCommands = (
|
||||
) => ElectronActiveObservationPort;
|
||||
} = {},
|
||||
): void => {
|
||||
registerElectronObservationCommands(cli, logger);
|
||||
registerElectronObservationCommands(cli, logger, environment);
|
||||
registerElectronFuseCommand(cli, logger);
|
||||
registerElectronActiveCommand(cli, logger, environment, options);
|
||||
registerJavaScriptApplicationCommand(cli, logger);
|
||||
registerJavaScriptApplicationCommand(cli, logger, environment);
|
||||
registerJavaScriptRuntimeReconciliationCommand(cli, logger);
|
||||
};
|
||||
|
||||
@@ -199,14 +199,16 @@ const registerJavaScriptRuntimeReconciliationCommand = (
|
||||
const registerElectronObservationCommands = (
|
||||
cli: ReturnType<typeof Cli.create>,
|
||||
logger: Logger,
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): void => {
|
||||
registerElectronTargetList(cli, logger);
|
||||
registerElectronPageInspection(cli, logger);
|
||||
registerElectronTargetList(cli, logger, environment);
|
||||
registerElectronPageInspection(cli, logger, environment);
|
||||
};
|
||||
|
||||
const registerElectronTargetList = (
|
||||
cli: ReturnType<typeof Cli.create>,
|
||||
logger: Logger,
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.listElectronTargets, {
|
||||
description:
|
||||
@@ -217,7 +219,7 @@ const registerElectronTargetList = (
|
||||
run: ({ args }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, "list-electron-targets", async () => {
|
||||
const context = await electronObservationContext();
|
||||
const context = await electronObservationContext(environment);
|
||||
const input = {
|
||||
cdp_endpoint: args.endpoint,
|
||||
};
|
||||
@@ -250,6 +252,7 @@ const registerElectronTargetList = (
|
||||
const registerElectronPageInspection = (
|
||||
cli: ReturnType<typeof Cli.create>,
|
||||
logger: Logger,
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectElectronPage, {
|
||||
description: "Passively inspect one Electron file page",
|
||||
@@ -261,7 +264,7 @@ const registerElectronPageInspection = (
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, "inspect-electron-page", async () => {
|
||||
const context = await electronObservationContext();
|
||||
const context = await electronObservationContext(environment);
|
||||
const input = {
|
||||
cdp_endpoint: args.endpoint,
|
||||
target_id: args.targetId,
|
||||
@@ -298,6 +301,7 @@ const registerElectronPageInspection = (
|
||||
const registerJavaScriptApplicationCommand = (
|
||||
cli: ReturnType<typeof Cli.create>,
|
||||
logger: Logger,
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.analyzeJavaScriptApplication, {
|
||||
description:
|
||||
@@ -344,6 +348,7 @@ const registerJavaScriptApplicationCommand = (
|
||||
? {}
|
||||
: { analysis_timeout_ms: options.analysisTimeoutMs }),
|
||||
},
|
||||
environment,
|
||||
signal,
|
||||
),
|
||||
),
|
||||
@@ -351,10 +356,12 @@ const registerJavaScriptApplicationCommand = (
|
||||
});
|
||||
};
|
||||
|
||||
const electronObservationContext = async () => {
|
||||
const electronObservationContext = async (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
) => {
|
||||
const { createElectronObservationProvider } =
|
||||
await import("../composition/electronObservation.js");
|
||||
return { provider: createElectronObservationProvider() };
|
||||
return { provider: createElectronObservationProvider(environment) };
|
||||
};
|
||||
|
||||
const inputError = (
|
||||
|
||||
+18
-3
@@ -10,6 +10,7 @@ import {
|
||||
deliverCliAnalysisEvidence,
|
||||
} from "./analysisEvidenceDelivery.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { machoSliceArchitectureSchema } from "../domain/apple/machoTargetSelection.js";
|
||||
|
||||
/** Resolve caller paths and inspect static build metadata through the shared workflow. */
|
||||
export const registerGoCommands = (cli: CliInstance, logger: Logger): void => {
|
||||
@@ -17,9 +18,18 @@ export const registerGoCommands = (cli: CliInstance, logger: Logger): void => {
|
||||
description:
|
||||
"Inspect embedded Go compiler, modules and build settings without target execution",
|
||||
args: z.object({
|
||||
path: z.string().describe("Local ELF, PE or thin Mach-O image"),
|
||||
path: z
|
||||
.string()
|
||||
.describe("Local ELF, PE, thin Mach-O or universal Mach-O image"),
|
||||
}),
|
||||
options: z.object({
|
||||
...analysisEvidenceDeliveryOptions,
|
||||
architecture: machoSliceArchitectureSchema
|
||||
.optional()
|
||||
.describe(
|
||||
"CPU family of the slice to read from a universal Mach-O; required for universal files",
|
||||
),
|
||||
}),
|
||||
options: z.object(analysisEvidenceDeliveryOptions),
|
||||
run: ({ args, options }) =>
|
||||
withCommandCancellation(async (signal) => {
|
||||
const service = createGoBinaryService();
|
||||
@@ -30,7 +40,12 @@ export const registerGoCommands = (cli: CliInstance, logger: Logger): void => {
|
||||
deliverCliAnalysisEvidence({
|
||||
run: () =>
|
||||
service.inspect(
|
||||
{ path: absoluteCallerPath(args.path) },
|
||||
{
|
||||
path: absoluteCallerPath(args.path),
|
||||
...(options.architecture === undefined
|
||||
? {}
|
||||
: { architecture: options.architecture }),
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
service,
|
||||
|
||||
@@ -46,6 +46,7 @@ export const closeCliJavaScriptResources = (): Promise<void> => {
|
||||
/** Execute the shared CLI boundary for static JavaScript analysis. */
|
||||
export const runCliJavaScriptApplicationAnalysis = async (
|
||||
input: unknown,
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<JsonValue> => {
|
||||
const { analyzeJavaScriptApplication } =
|
||||
@@ -58,6 +59,7 @@ export const runCliJavaScriptApplicationAnalysis = async (
|
||||
);
|
||||
const resources = cliAnalysisScope();
|
||||
const result = await analyzeJavaScriptApplication(input, resources, {
|
||||
environment,
|
||||
progress,
|
||||
...(signal === undefined ? {} : { signal }),
|
||||
});
|
||||
|
||||
+22
-2
@@ -15,6 +15,7 @@ import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
import { absoluteCallerPath } from "../filesystem/callerPath.js";
|
||||
import { machoSliceArchitectureSchema } from "../domain/apple/machoTargetSelection.js";
|
||||
|
||||
/** Resolve caller paths and inspect embedded Rust dependencies through the shared workflow. */
|
||||
export const registerRustCommands = (
|
||||
@@ -25,10 +26,17 @@ export const registerRustCommands = (
|
||||
description:
|
||||
"Inspect an embedded cargo-auditable Rust dependency list without target execution",
|
||||
args: z.object({
|
||||
path: z.string().describe("Local ELF, PE or thin Mach-O image"),
|
||||
path: z
|
||||
.string()
|
||||
.describe("Local ELF, PE, thin Mach-O or universal Mach-O image"),
|
||||
}),
|
||||
options: z.object({
|
||||
...analysisEvidenceDeliveryOptions,
|
||||
architecture: machoSliceArchitectureSchema
|
||||
.optional()
|
||||
.describe(
|
||||
"CPU family of the slice to read from a universal Mach-O; required for universal files",
|
||||
),
|
||||
detail: z
|
||||
.enum(["complete", "summary"])
|
||||
.optional()
|
||||
@@ -67,6 +75,9 @@ export const registerRustCommands = (
|
||||
const view = decoded.value;
|
||||
const input = inspectRustBinaryRequestSchema.safeParse({
|
||||
path: absoluteCallerPath(args.path),
|
||||
...(options.architecture === undefined
|
||||
? {}
|
||||
: { architecture: options.architecture }),
|
||||
...(options.detail === undefined ? {} : { detail: options.detail }),
|
||||
...(view === undefined ? {} : { view }),
|
||||
});
|
||||
@@ -80,7 +91,16 @@ export const registerRustCommands = (
|
||||
);
|
||||
const service = createRustBinaryService();
|
||||
return deliverCliAnalysisEvidence({
|
||||
run: () => service.inspect({ path: input.data.path }, { signal }),
|
||||
run: () =>
|
||||
service.inspect(
|
||||
{
|
||||
path: input.data.path,
|
||||
...(input.data.architecture === undefined
|
||||
? {}
|
||||
: { architecture: input.data.architecture }),
|
||||
},
|
||||
{ signal },
|
||||
),
|
||||
service,
|
||||
providerId: "rust-auditable",
|
||||
operation: "inspect_rust_binary",
|
||||
|
||||
@@ -65,6 +65,7 @@ export const CLI_COMMANDS = Object.freeze({
|
||||
inspectGpuCodeObjects: "inspect-gpu-code-objects",
|
||||
inspectRustBinary: "inspect-rust-binary",
|
||||
inspectPeResources: "inspect-pe-resources",
|
||||
inspectPeCertificates: "inspect-pe-certificates",
|
||||
inspectAnalysisView: "inspect-analysis-view",
|
||||
listFridaDevices: "list-frida-devices",
|
||||
listFridaProcesses: "list-frida-processes",
|
||||
|
||||
@@ -37,6 +37,7 @@ const observeOptionsSchema = z.object({
|
||||
export const registerJavaScriptRuntimeObservationCommands = (
|
||||
cli: ReturnType<typeof Cli.create>,
|
||||
logger: Logger,
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.listJavaScriptRuntimeTargets, {
|
||||
description: "List Node/Electron V8 Inspector targets",
|
||||
@@ -49,7 +50,7 @@ export const registerJavaScriptRuntimeObservationCommands = (
|
||||
logger,
|
||||
CLI_COMMANDS.listJavaScriptRuntimeTargets,
|
||||
async () => {
|
||||
const context = runtimeContext();
|
||||
const context = runtimeContext(environment);
|
||||
const input = {
|
||||
inspector_endpoint: args.endpoint,
|
||||
};
|
||||
@@ -94,7 +95,7 @@ export const registerJavaScriptRuntimeObservationCommands = (
|
||||
logger,
|
||||
CLI_COMMANDS.observeJavaScriptRuntime,
|
||||
async () => {
|
||||
const context = runtimeContext();
|
||||
const context = runtimeContext(environment);
|
||||
const input = {
|
||||
inspector_endpoint: args.endpoint,
|
||||
target_id: args.targetId,
|
||||
@@ -127,8 +128,10 @@ export const registerJavaScriptRuntimeObservationCommands = (
|
||||
});
|
||||
};
|
||||
|
||||
const runtimeContext = () => ({
|
||||
provider: createJavaScriptRuntimeObservationProvider(),
|
||||
const runtimeContext = (
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
) => ({
|
||||
provider: createJavaScriptRuntimeObservationProvider(environment),
|
||||
});
|
||||
|
||||
const cliError = (
|
||||
|
||||
@@ -4,11 +4,17 @@ import { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayo
|
||||
import { PwntoolsLayoutProvider } from "../native/pwntools/PwntoolsLayoutProvider.js";
|
||||
import { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js";
|
||||
import { PeResourcesProvider } from "../native/pe/PeResourcesProvider.js";
|
||||
import { PeCertificatesService } from "../application/binaryDiagnostics/PeCertificatesService.js";
|
||||
import { PeCertificatesProvider } from "../native/pe/PeCertificatesProvider.js";
|
||||
|
||||
/** Portable PE inspection with no engine, installation or target acquisition. */
|
||||
export const createPeResourcesService = (): PeResourcesService =>
|
||||
new PeResourcesService(new PeResourcesProvider());
|
||||
|
||||
/** Portable Authenticode table inspection with no engine or catalog verification. */
|
||||
export const createPeCertificatesService = (): PeCertificatesService =>
|
||||
new PeCertificatesService(new PeCertificatesProvider());
|
||||
|
||||
/** Compose the offline adapter without import-time I/O or dependency acquisition. */
|
||||
export const createBinaryLayoutService = (
|
||||
environment: Readonly<NodeJS.ProcessEnv> = process.env,
|
||||
|
||||
Loaded 100 of 344 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user