Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9940688de4 | ||
|
|
a9195c3a98 | ||
|
|
4758ba30b6 | ||
|
|
64041e3072 | ||
|
|
39f96d7188 | ||
|
|
02095cd74a | ||
|
|
26525706fc | ||
|
|
45a3509db3 | ||
|
|
e4186af31b | ||
|
|
42667fc1a0 | ||
|
|
729ca2ab6a | ||
|
|
4481f86188 | ||
|
|
72d5bbc96d | ||
|
|
97268f40ea | ||
|
|
388b907c92 | ||
|
|
0e6eca4085 | ||
|
|
c7ceaadb70 | ||
|
|
090b454350 | ||
|
|
0bc422d7be | ||
|
|
2895a18ca9 | ||
|
|
93ee758e9d | ||
|
|
7a6defab18 | ||
|
|
4beb749ae3 | ||
|
|
8fef2aa64e | ||
|
|
7465b0ef15 | ||
|
|
5622b5cbb8 | ||
|
|
6be1d02db5 | ||
|
|
e6830ba4b6 | ||
|
|
1148801f06 | ||
|
|
07bb25bff0 | ||
|
|
f505fa2564 | ||
|
|
ce33b3c32d |
@@ -122,6 +122,14 @@ explicit path or endpoint and do not need it.
|
||||
absent metadata does not prove a binary is not Go, and unparsed text records
|
||||
remain explicit. See the
|
||||
[Go binary guide](https://github.com/morluto/rea/blob/main/docs/go-binaries.md).
|
||||
- Embedded cargo-auditable Rust dependency metadata: `inspect_rust_binary`
|
||||
with the explicit local ELF, PE, or thin Mach-O file. This built-in
|
||||
target-free inspection requires no Rust installation or disassembler. Preserve
|
||||
package order, dependency indices, and the section file range. A missing
|
||||
`.dep-v0` section is null metadata, not proof the binary has no dependencies.
|
||||
A rustc commit hash is an inference from a read-only `/rustc/<40-hex>/` path.
|
||||
See the
|
||||
[Rust binary guide](https://github.com/morluto/rea/blob/main/docs/rust-binaries.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
|
||||
|
||||
@@ -11,6 +11,18 @@ unparsed module-text line. A missing record is an unknown, not proof of a
|
||||
non-Go binary; observed metadata does not authenticate its claims. Function,
|
||||
source and type recovery remain a separate native-analysis question.
|
||||
|
||||
## Rust dependency metadata
|
||||
|
||||
Use `inspect_rust_binary` for an explicit local ELF, PE, or thin Mach-O 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`.
|
||||
Preserve artifact SHA-256, package order, dependency indices, and the section
|
||||
file range. A missing section is null metadata, not proof the binary has no
|
||||
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).
|
||||
|
||||
## SQLite snapshots
|
||||
|
||||
Use `inspect_sqlite_database` with an explicit local database snapshot for its
|
||||
|
||||
@@ -33,7 +33,7 @@ env:
|
||||
jobs:
|
||||
changes:
|
||||
name: Classify changes
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -290,14 +290,19 @@ jobs:
|
||||
- if: needs.changes.outputs.code == 'true' && success()
|
||||
run: npm ci
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
if: needs.changes.outputs.code == 'true' && success()
|
||||
if: >-
|
||||
!cancelled() && needs.changes.outputs.code == 'true' &&
|
||||
(needs.test-shard.result == 'success' || needs.test-shard.result == 'failure')
|
||||
with:
|
||||
path: .vitest/blob
|
||||
pattern: vitest-report-*
|
||||
merge-multiple: true
|
||||
- name: Merge coverage, JUnit, and timing reports
|
||||
id: merge
|
||||
if: needs.changes.outputs.code == 'true' && needs.changes.outputs.tier == 'full' && success()
|
||||
if: >-
|
||||
!cancelled() && needs.changes.outputs.code == 'true' &&
|
||||
needs.changes.outputs.tier == 'full' &&
|
||||
(needs.test-shard.result == 'success' || needs.test-shard.result == 'failure')
|
||||
run: >-
|
||||
npm run test:ci:merge --
|
||||
--coverage.reportsDirectory=coverage
|
||||
@@ -312,7 +317,10 @@ jobs:
|
||||
--outputFile.json=test-results.json
|
||||
- name: Merge fast-suite JUnit and timing reports
|
||||
id: merge-fast
|
||||
if: needs.changes.outputs.code == 'true' && needs.changes.outputs.tier == 'fast' && success()
|
||||
if: >-
|
||||
!cancelled() && needs.changes.outputs.code == 'true' &&
|
||||
needs.changes.outputs.tier == 'fast' &&
|
||||
(needs.test-shard.result == 'success' || needs.test-shard.result == 'failure')
|
||||
run: >-
|
||||
npm exec -- vitest --merge-reports
|
||||
--reporter=default
|
||||
@@ -777,7 +785,8 @@ jobs:
|
||||
|
||||
required:
|
||||
name: CI required
|
||||
if: ${{ always() }}
|
||||
# Report failed dependencies, but release superseded PR concurrency slots.
|
||||
if: ${{ !cancelled() }}
|
||||
needs:
|
||||
[
|
||||
changes,
|
||||
@@ -804,7 +813,7 @@ jobs:
|
||||
nativeaot,
|
||||
website,
|
||||
]
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
|
||||
|
||||
@@ -14,7 +14,7 @@ concurrency:
|
||||
jobs:
|
||||
pr-title:
|
||||
name: Conventional PR title
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
- name: Validate title for Release Please
|
||||
|
||||
@@ -181,6 +181,10 @@ The separate GitHub Pages host uses only the manual
|
||||
|
||||
## Development feedback and PR verification
|
||||
|
||||
For routine merges, wait for the required checks to pass for the current PR
|
||||
revision. Administrators retain an override for deliberate maintainer recovery.
|
||||
If `main` is broken, prioritize a focused repair before updating affected PRs.
|
||||
|
||||
Select checks for the behavior changed. For a source edit:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -152,15 +152,16 @@ Additional tools and host support depend on the target:
|
||||
|
||||
| Target | What REA returns | Requirements and guide |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](https://rea.tools/guides/native/) |
|
||||
| Native binaries | Pseudocode, assembly, strings, symbols, calls and references | Hopper, Ghidra or IDA; [native analysis](docs/native-investigation.md) |
|
||||
| Offline ELF layout | Sections, segments, original symbols/relocations and static mitigation candidates | Caller-supplied pwntools on Linux x64; [binary diagnostics](docs/binary-diagnostics.md) |
|
||||
| Go binaries | Embedded compiler version, modules, replacements and build settings | Built-in static ELF/PE/thin Mach-O inspection; no Go installation required |
|
||||
| Rust binaries | Embedded cargo-auditable crate names, versions, sources and dependency edges | Built-in static ELF/PE/thin Mach-O inspection; [Rust binaries](docs/rust-binaries.md) |
|
||||
| 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 |
|
||||
| Recorded Linux crashes | Raw notes, every recorded thread's registers/signals and optional mapping candidates | Caller-supplied pwntools on Linux x64; optional GDB/pwndbg; [recorded crashes](docs/recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; [application analysis](https://rea.tools/guides/javascript/) |
|
||||
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](https://rea.tools/guides/browser/) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC and native add-on relationships | Node.js and npm; [application analysis](docs/javascript-artifact-reconstruction.md) |
|
||||
| Websites | Page structure, scripts, network observations and requested screenshots | A Chrome-family browser; [browser analysis](docs/browser-observation.md) |
|
||||
| Saved network captures | Requests, responses, exposed payloads and source locations | HAR or Fiddler Everywhere SAZ; mitmdump on Linux for native mitmproxy; [capture guide](docs/web-network-captures.md) |
|
||||
| .NET assemblies | Metadata, CIL instructions, declared native dependencies and build comparisons | None (static only); [managed-code guide](docs/managed-code-analysis.md) |
|
||||
| Android APKs | Manifest declarations, classes, decompiled methods and references | Headless JADX and a full JDK on Linux/macOS/Windows x64; [Android guide](docs/android-analysis.md) |
|
||||
|
||||
@@ -29,6 +29,7 @@ export default defineConfig({
|
||||
items: [
|
||||
{ text: "Native binaries", link: "/native-investigation" },
|
||||
{ text: "Go build metadata", link: "/go-binaries" },
|
||||
{ text: "Rust dependencies", link: "/rust-binaries" },
|
||||
{
|
||||
text: "JavaScript and Electron",
|
||||
link: "/javascript-artifact-reconstruction",
|
||||
|
||||
@@ -243,6 +243,7 @@ REA retains local observation data and removes only URL userinfo credentials, ex
|
||||
- Console observations with a stack source retain call type, argument types, timestamp, and source location. When console text capture is selected, already-delivered primitive values are retained verbatim; objects, getters, and remote properties are never expanded.
|
||||
- WebSocket observations retain direction, opcode, and payload byte length. When shape capture is selected, text frames are classified as text or complete value-free JSON shape; binary bytes, hashes, prefixes, and raw frames are never retained.
|
||||
- Storage observations always redact values. Key names, IndexedDB names, and cache names require `include_storage_keys`; stable content fingerprints require the additional `include_storage_fingerprints` selection. Cookie fingerprints cover cookies applicable to the current main-frame URL, including its path, rather than every cookie path on that origin. Other storage remains scoped to the origin. Cache bodies above 64 KiB and partial IndexedDB remote objects make fingerprint coverage incomplete, so identical observations remain `unknown`.
|
||||
- Unavailable or malformed storage inventories mark `storage_keys` unavailable, even without fingerprints. Comparisons suppress inferred key additions and removals while preserving modifications proven by complete fingerprints on both sides. Previously exported captures are not retroactively corrected; recapture them to obtain these coverage markers.
|
||||
- Script metadata is included only when CDP supplies a URL on an allowed origin. Stable keys exclude transient CDP script IDs, and exact transient raw URLs are used only during script/resource reconciliation. URL-less scripts are excluded because their origin cannot be established. When CDP supplies an execution-context association, the accepted script retains its authorized frame ID for later attribution. Script metadata admission bounds retained identity and source-map strings to 8 MiB and 50,000 records. When a source-map URL cannot fit, the script identity remains and source-map coverage reports `resource_budget_exhausted`; rejected script observations are counted without retaining their IDs. Source content is omitted unless explicitly requested and is returned inline as a self-verifying artifact with its SHA-256 digest.
|
||||
|
||||
Frames, resources, scripts, events, and workers outside the selected origin scope are excluded. An explicit `allowed_origins` list can include additional origins; excluded target details are counted without being exposed.
|
||||
|
||||
+12
@@ -83,6 +83,18 @@ The shared CLI/MCP workflow reports compiler version, modules, replacements,
|
||||
build settings, original bytes and SHA-256. See [Go binaries](go-binaries.md)
|
||||
for supported containers and absent or malformed metadata.
|
||||
|
||||
Inspect an embedded cargo-auditable Rust dependency list without installing
|
||||
Rust or configuring a disassembler:
|
||||
|
||||
```bash
|
||||
rea inspect-rust-binary /absolute/path/to/program --json
|
||||
```
|
||||
|
||||
The same workflow reports package names, versions, sources, the root package,
|
||||
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).
|
||||
|
||||
Configure [Hopper or Ghidra](installation.md#hopper), or the
|
||||
[IDA adapter](ida-provider.md), before analyzing a native target. Substitute
|
||||
your target, search text and function name or address in these examples:
|
||||
|
||||
@@ -6,8 +6,8 @@ For remote devices, pass a host with an optional port (`host[:port]`; bracket IP
|
||||
|
||||
MCP exposes `list_frida_devices`, `list_frida_processes`, `start_frida_session`, `load_frida_script`, `resume_frida_session`, `unload_frida_script`, `frida_session_status`, and `close_frida_session`. A spawned process starts paused so scripts can load first. Closing unloads session scripts and detaches. If a spawn remains paused, REA resumes it only after script cleanup or session detach confirms the instrumentation is released; if both cleanup paths fail, the target stays paused and the cleanup error is retained. REA never kills the target. MCP sessions end with the server process. The one-shot CLI command `instrument-with-frida` accepts one inline script or script file, observes messages for a bounded duration, and returns Evidence with the source hash and captured messages.
|
||||
|
||||
Failed startup cleanup retains acquired Frida handles, including a paused spawn that has not attached, for later cleanup retries. Its remote connection stays leased until session detach and target resume complete. CLI commands await provider cleanup before returning; MCP retains these resources for cleanup when its server closes. A failed cleanup remains reported rather than treating an unregistered session as fully released.
|
||||
Failed startup cleanup retains acquired Frida handles, including a paused spawn that has not attached, for later cleanup retries. Its remote connection stays leased until session detach and target resume complete. CLI commands await provider cleanup before returning; MCP retains these resources for cleanup when its server closes. Server shutdown stops admitting new Frida operations and waits for admitted startup, discovery, and queued work before releasing their resources. A failed cleanup remains reported and can be retried by closing the server again, rather than treating an unregistered session as fully released.
|
||||
|
||||
Frida scripts execute inside the selected target with that process's memory, filesystem, and network authority. Message capture is bounded to 16 MiB and reports truncation. Script source is not stored in Evidence; its selected path and SHA-256 digest are. Evidence retains non-authentication target selection such as device or endpoint, mode, program or PID, and spawn arguments. Authentication values remain excluded. Results report the observed session and messages, while unsupported platform metadata remains unknown. A request cancelled before a Frida operation starts has no provider effects. If cancellation arrives during a native Frida call, REA waits for that call to return, then closes any session it created and returns the observed session as a partial observation. Cancellation Evidence separately reports whether a target resume was observed, whether instrumentation cleanup completed, and that final target liveness remains unknown. REA does not kill the target. Remote discovery retains its observations and reports a separate cleanup error if the temporary connection cannot be removed.
|
||||
Frida scripts execute inside the selected target with that process's memory, filesystem, and network authority. Message capture is bounded to 16 MiB and reports truncation. Script source is not stored in Evidence; its selected path and SHA-256 digest are. Evidence retains non-authentication target selection such as device or endpoint, mode, program or PID, and spawn arguments. Authentication values remain excluded. Results report the observed session and messages, while unsupported platform metadata remains unknown. A request cancelled before a Frida operation starts has no provider effects. If cancellation arrives during a native Frida call, REA waits for that call to return, then closes any session it created and returns the observed session as a partial observation. A cancelled script load on an existing session releases only its new script before queued operations can run; the session and previously loaded scripts remain available. Once the provider commits a successful load, later request cancellation does not unload that script; use `unload_frida_script` to release it. If script cleanup fails, the script remains owned and a paused target cannot resume until the pending instrumentation is released. Queued script loads, resumes, and unloads check cancellation before dispatching native effects. Cancellation Evidence separately reports whether a target resume was observed, whether instrumentation cleanup completed, and that final target liveness remains unknown. REA does not kill the target. Remote discovery retains its observations and reports a separate cleanup error if the temporary connection cannot be removed.
|
||||
|
||||
The [Frida Core API](https://frida.re/docs/frida-core-api/) documents device management, sessions, and script loading; the [JavaScript API](https://frida.re/docs/javascript-api/) documents target-side instrumentation. [Luma](https://luma.frida.re/) is Frida's standalone GUI; REA does not automate or control it. `npm run verify:frida` requires a Linux host with the optional Frida binding and uses a short-lived local Node.js fixture created by the verifier. Add `-- --address HOST:PORT --pid PID` to run the separate remote attach lane against a caller-selected target. Mock tests do not establish real device/server compatibility.
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ For runtime instrumentation, see the [Frida guide](./frida-instrumentation.md).
|
||||
|
||||
| Task | Guides |
|
||||
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Inspect native functions, references, or types | [Native binaries](native-investigation.md), [Go metadata](go-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), [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), [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) |
|
||||
|
||||
@@ -764,6 +764,9 @@ errors expose Error names, messages, and available codes under
|
||||
truncation markers. Darwin ownership-helper compilation failures also retain
|
||||
the compiler outcome, exit code or signal, and bounded stderr; a timed-out
|
||||
compiler remains distinguishable from a compiler that exited with an error.
|
||||
The helper compile deadline defaults to 120,000 ms; set
|
||||
`REA_SWIFTC_COMPILE_TIMEOUT_MS` to an integer between 1 and 2,147,483,647 when
|
||||
a busy host needs longer.
|
||||
Other rejection values retain their primitive value or an explicit type.
|
||||
Shutdown warnings include the failure
|
||||
kind, message, and diagnostics, while process and temporary-project cleanup
|
||||
|
||||
@@ -139,14 +139,40 @@ The result preserves the requested names in `selection.type_names` and reports
|
||||
unmatched names separately. If metadata names are unreadable, limitations retain
|
||||
that uncertainty. With no selection, `selection` is null and whole-assembly
|
||||
inspection keeps its existing resource policy. Selection restricts types,
|
||||
fields, methods, call edges and field accesses; member references remain an
|
||||
assembly-wide inventory, so output size is not guaranteed to be small.
|
||||
fields, methods, call edges, field accesses and user strings; member references
|
||||
remain an assembly-wide inventory, so output size is not guaranteed to be small.
|
||||
|
||||
Coverage describes the selected members. Comparisons leave members outside the
|
||||
opposite observation's selection unknown, and application graphs report their
|
||||
selected scope as partial. Neither makes an absence claim about the whole
|
||||
assembly. Older member Evidence without `selection` remains readable.
|
||||
|
||||
### User strings
|
||||
|
||||
`ldstr` anchors keep their build-local `0x70` tokens. `user_strings.literals`
|
||||
lists each distinct token loaded by an inspected method body once, in token
|
||||
order, with its `#US` file offset and decoded value. Values keep their stored
|
||||
UTF-16 code units, including unpaired surrogates. Token `0x70000000` names the
|
||||
heap's empty entry at offset 0 and reads as `""`; any other item there is
|
||||
malformed. The terminal flag is checked against ECMA-335 II.24.2.4, and the
|
||||
length prefix against the width II.23.2 assigns. A difference is reported with
|
||||
its byte offset as a user-string issue while preserving the readable literal.
|
||||
Toolchain conventions can differ; this conformance observation does not
|
||||
establish runtime rejection. A missing `#US` stream is one stream-level issue;
|
||||
a malformed item is reported against its token. User strings carry their own
|
||||
coverage: any of these makes `user_strings.state` partial and lists the issue
|
||||
in `user_strings.issues`, while the member `coverage` and `metadata.status` are
|
||||
unaffected, and readable literals remain. The state is also partial when an
|
||||
inspected method's CIL was not fully decoded (a malformed exception section
|
||||
alone does not count) or a method that may belong to the inspected types was
|
||||
not inspected (its row could not be read, or its type's name could not be read
|
||||
under a `type_names` selection), because `ldstr` instructions there were not
|
||||
observed. Normalized CIL digests do not include decoded values.
|
||||
`compare_managed_members` does not decode or compare literals: an unchanged
|
||||
method does not establish unchanged strings, and the comparison says so in its
|
||||
limitations. Older member Evidence without `user_strings` reads as `null`: not
|
||||
examined.
|
||||
|
||||
### Edge target declaring types
|
||||
|
||||
`member_refs[].parent_name`, `call_edges[].target_declaring_type`, and
|
||||
@@ -183,9 +209,11 @@ native/runtime questions retain their own authority and coverage.
|
||||
Names and semantic labels do not establish durable member identity. Comparison
|
||||
preserves observed differences, structural inferences, and unknowns separately.
|
||||
|
||||
Cross-version matching first prefers exact CIL/signature identity. When that
|
||||
does not match, it pairs an exact declared type, method name, and raw signature
|
||||
before trying structural body shape. The exact-signature key uses names only
|
||||
Cross-version matching first prefers exact raw CIL/signature identity, with
|
||||
equal decoded fingerprints. Equal decoded fingerprints alone cannot establish
|
||||
an exact match: they omit switch targets and collapse distinct NaN encodings.
|
||||
When that does not match, it pairs an exact declared type, method name, and raw
|
||||
signature before trying structural body shape. The exact-signature key uses names only
|
||||
as part of that full tuple; names alone never select a pair. Duplicate tuples
|
||||
remain ambiguous rather than being paired by token order. Fields follow the
|
||||
same exact tiers. The raw signature is exact whether or not REA decoded it, so
|
||||
@@ -196,8 +224,11 @@ unpaired member, one-sided or ambiguous, with the same declared type and name
|
||||
whose signature was not decoded.
|
||||
|
||||
For a matched method, an unavailable or partial body makes body-shape facets
|
||||
unknown while preserving observed signature differences. Structural identity
|
||||
compares normalized signatures, the limited decoded-CIL tuple fingerprint,
|
||||
unknown while preserving observed signature differences. For complete bodies,
|
||||
a difference in either the raw CIL digest or the decoded fingerprint is reported
|
||||
in the `cil` dimension, including reordered switch targets with equal decoded
|
||||
fingerprints. Structural identity compares normalized signatures, the limited
|
||||
decoded-CIL tuple fingerprint,
|
||||
constants, and bounded shape context. It reports all candidates at the winning
|
||||
score and remains ambiguous when the evidence does not distinguish them. The
|
||||
digest does not itself remap metadata tokens.
|
||||
@@ -334,8 +365,8 @@ Opcode names retain short/long forms and prefix opcodes. This makes the value a
|
||||
reproducible decoded-tuple fingerprint, not a canonical semantic CIL identity:
|
||||
it does not resolve tokens across MVIDs, does not commit full control flow or
|
||||
exception semantics, and does not replace the raw CIL digest. Cross-build
|
||||
comparison uses the value only as one unique-only structural tier and reports
|
||||
ambiguity when that evidence is insufficient.
|
||||
comparison requires equal raw CIL digests as well for exact CIL/signature
|
||||
matches and reports ambiguity when structural evidence is insufficient.
|
||||
|
||||
For example, a decoded `nop; ret` body serializes exactly as
|
||||
`[["nop","none",null],["ret","none",null]]` and has digest
|
||||
|
||||
@@ -21,6 +21,31 @@ directly to `compare_functions`, and `inspect_artifact` Evidence to
|
||||
`compare_artifacts`. Use `get_evidence_bundle` when the task needs broader
|
||||
retained session history or an explicit bundle for transfer.
|
||||
|
||||
Artifact inventories declare `coverage.scope` (`root` or `container-entries`)
|
||||
and `coverage.status` (`complete` or `unknown`) independently of diagnostic
|
||||
wording. Comparison requires matching scopes, complete graph members and complete
|
||||
traversal coverage before concluding equality. A fully observed root without
|
||||
container children can be `unchanged`; its scope notice stays in `limitations`
|
||||
without creating a gap. Content and executable metadata changes still produce
|
||||
root deltas. Missing graph members yield `truncated`; unknown traversal coverage
|
||||
or unavailable occurrence hashes retain uncertainty. Integrity mismatches remain
|
||||
contradictions. Static artifact equality does not establish unchanged runtime
|
||||
behavior.
|
||||
|
||||
Exhausted traversals with deliberately omitted filesystem entries retain
|
||||
`unknown` coverage and identify each omitted path/type in `limitations`; FIFO,
|
||||
socket and device entries are not opened. Case-distinct archive members remain
|
||||
separate observations. Their case-insensitive materialization diagnostic does
|
||||
not reduce traversal coverage when both spellings were read.
|
||||
|
||||
Current output also retains `coverage_status` (`complete` or `partial`) for existing clients, derived from the scoped traversal status. The owning parser normalizes retained `coverage_status` records to scoped coverage and rejects conflicting declarations.
|
||||
|
||||
The coverage field is optional when reading older retained Evidence. The owning
|
||||
inventory parser recognizes historical REA scope notices for compatibility;
|
||||
unfamiliar legacy limitations remain conservative. Typed records do not derive
|
||||
coverage from limitation text. Pages from one inventory must carry the same
|
||||
coverage declaration.
|
||||
|
||||
Function dossiers can declare per-facet coverage in the optional
|
||||
`normalized_result.facet_availability` map. Each supplied facet is either
|
||||
`{ "available": true }` or `{ "available": false, "reason": "..." }`.
|
||||
@@ -474,6 +499,7 @@ These deadlines have different owners:
|
||||
| MCP initialize | The client bounds transport/REA connection startup, before any target query. |
|
||||
| Hopper startup | REA allows 120,000 ms by default; `REA_HOPPER_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid values fail configuration validation. Startup failure is reported by the first provider query. |
|
||||
| Ghidra startup | REA allows 330,000 ms by default; `REA_GHIDRA_STARTUP_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid supplied values fail configuration validation. Startup failure is reported by the first provider query. |
|
||||
| Darwin swiftc helper | REA allows 120,000 ms by default for ownership-helper compilation; `REA_SWIFTC_COMPILE_TIMEOUT_MS` accepts decimal integers from 1–2,147,483,647 ms. Invalid values fail configuration validation. |
|
||||
| Individual tool call | The client bounds its wait, including cold engine startup. The pinned client SDK 2.3.1 defaults to 60,000 ms and can cancel earlier than REA's startup deadline. |
|
||||
|
||||
For an already connected client using the pinned SDK, request options are the
|
||||
|
||||
@@ -132,12 +132,13 @@ npx rea-agents@latest setup
|
||||
|
||||
| الهدف | ما يعيده REA | المتطلبات والدليل |
|
||||
| ------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
|
||||
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](https://rea.tools/ar/guides/native/) |
|
||||
| الملفات الثنائية للشيفرة الأصلية | شيفرة شبه برمجية، وتعليمات تجميع، وسلاسل نصية، ورموز، واستدعاءات، ومراجع | Hopper أو Ghidra أو IDA؛ [تحليل الشيفرة الأصلية](../native-investigation.md) |
|
||||
| بنية ELF دون تشغيل | الأقسام والمقاطع والرموز ومعلومات إعادة التموضع الأصلية، وآليات الحماية المحتملة المستدلّ عليها بالفحص الثابت | pwntools يوفّره المستدعي على Linux x64؛ [تشخيص الملفات الثنائية](../binary-diagnostics.md) |
|
||||
| شيفرة EVM البايتية | محددات التوجيه، وإزاحات البايتات، والمعاملات المستنتجة، وقابلية تغيير الحالة | مدخل محلي يحمل بايتات خامًا أو تمثيلًا سداسيًا عشريًا على Linux x64؛ [دليل EVM دون تشغيل](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| أعطال Linux المسجّلة | سجلات note الخام، ومسجّلات المعالج/الإشارات لكل خيط مسجّل، وترشيحات اختيارية لخرائط الذاكرة | pwntools يوفّره المستدعي على Linux x64؛ GDB/pwndbg اختياريان؛ [الأعطال المسجّلة](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](https://rea.tools/ar/guides/javascript/) |
|
||||
| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](https://rea.tools/ar/guides/browser/) |
|
||||
| JavaScript / Electron | الوحدات، وعمليات الاستيراد، وخرائط المصدر، والمسارات، وIPC، وعلاقات إضافات الشيفرة الأصلية | Node.js وnpm؛ [تحليل التطبيقات](../javascript-artifact-reconstruction.md) |
|
||||
| المواقع | بنية الصفحة، والسكربتات، وملاحظات الشبكة، ولقطات الشاشة المطلوبة | متصفح من عائلة Chrome؛ [تحليل المتصفح](../browser-observation.md) |
|
||||
| تسجيلات الشبكة المحفوظة | الطلبات، والاستجابات، ومحتويات الحمولة المتاحة، ومواضعها في المصدر | HAR؛ وmitmdump على Linux لتسجيلات mitmproxy بصيغتها الأصلية؛ [دليل تسجيلات الشبكة](../web-network-captures.md) |
|
||||
| التقاط الحزم | نقاط النهاية وDNS وTLS SNI وتوزيع البروتوكولات وطلبات HTTP | tshark وcapinfos يوفّرهما المُستدعي؛ [دليل pcap](../pcap-analysis.md) |
|
||||
| تجميعات .NET | البيانات الوصفية، وتعليمات CIL، واعتماديات الشيفرة الأصلية المعلنة، ومقارنات البناء | فحص ثابت؛ [دليل الشيفرة المُدارة](../managed-code-analysis.md) |
|
||||
|
||||
@@ -132,12 +132,13 @@ REA benötigt Node.js 22.x (>=22.19), 24.x (>=24.11) oder 26+ sowie npm. Zusätz
|
||||
|
||||
| Ziel | Was REA zurückgibt | Voraussetzungen und Anleitung |
|
||||
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](https://rea.tools/de/guides/native/) |
|
||||
| Native Binärdateien | Pseudocode, Assemblercode, Zeichenketten, Symbole, Aufrufe und Verweise | Hopper, Ghidra oder IDA; [native Analyse](../native-investigation.md) |
|
||||
| Offline-ELF-Struktur | Sektionen, Segmente, ursprüngliche Symbole/Relokationen und mögliche Schutzmaßnahmen aus statischer Analyse | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; [Binärdiagnostik](../binary-diagnostics.md) |
|
||||
| EVM-Bytecode | Dispatch-Selektoren, Byte-Offsets, abgeleitete Argumente und Veränderbarkeit des Zustands | Lokale Eingabe mit Rohbytes oder Hexdarstellung unter Linux x64; [Offline-EVM-Leitfaden](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Aufgezeichnete Linux-Abstürze | Unverarbeitete Note-Einträge, Register/Signale aller aufgezeichneten Threads und optionale Zuordnungskandidaten | Vom Aufrufer bereitgestelltes pwntools unter Linux x64; optional GDB/pwndbg; [aufgezeichnete Abstürze](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](https://rea.tools/de/guides/javascript/) |
|
||||
| Websites | Seitenstruktur, Skripte, Netzwerkbeobachtungen und angeforderte Screenshots | Ein Browser aus der Chrome-Familie; [Browseranalyse](https://rea.tools/de/guides/browser/) |
|
||||
| JavaScript / Electron | Module, Imports, Source Maps, Routen, IPC und Beziehungen zu nativen Add-ons | Node.js und npm; [Anwendungsanalyse](../javascript-artifact-reconstruction.md) |
|
||||
| Websites | Seitenstruktur, Skripte, Netzwerkbeobachtungen und angeforderte Screenshots | Ein Browser aus der Chrome-Familie; [Browseranalyse](../browser-observation.md) |
|
||||
| Gespeicherte Netzwerkaufzeichnungen | Anfragen, Antworten, zugängliche Nutzdaten und Quellpositionen | HAR; mitmdump unter Linux für Aufzeichnungen im nativen mitmproxy-Format; [Leitfaden zu Netzwerkaufzeichnungen](../web-network-captures.md) |
|
||||
| Paketerfassungen | Endpunkte, DNS, TLS SNI, Protokollverteilung und HTTP-Anfragen | Vom Aufrufer bereitgestellte tshark- und capinfos-Binaries; [pcap-Leitfaden](../pcap-analysis.md) |
|
||||
| .NET-Assemblies | Metadaten, CIL-Anweisungen, deklarierte native Abhängigkeiten und Build-Vergleiche | Statische Untersuchung; [Leitfaden für verwalteten Code](../managed-code-analysis.md) |
|
||||
|
||||
@@ -132,12 +132,13 @@ REA requiere Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, además de npm. Las h
|
||||
|
||||
| Objetivo | Qué devuelve REA | Requisitos y guía |
|
||||
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](https://rea.tools/es/guides/native/) |
|
||||
| Binarios nativos | Pseudocódigo, ensamblador, cadenas, símbolos, llamadas y referencias | Hopper, Ghidra o IDA; [análisis nativo](../native-investigation.md) |
|
||||
| Estructura ELF sin ejecutar | Secciones, segmentos, símbolos/reubicaciones originales y posibles medidas de mitigación detectadas mediante análisis estático | pwntools proporcionado por quien llama, en Linux x64; [diagnóstico de binarios](../binary-diagnostics.md) |
|
||||
| Bytecode EVM | Selectores de despacho, desplazamientos en bytes, argumentos inferidos y mutabilidad | Entrada local con bytes sin procesar o representación hexadecimal, en Linux x64; [guía de EVM sin ejecutar](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Fallos de Linux registrados | Registros note sin procesar, registros/señales de cada hilo registrado y posibles correspondencias de memoria opcionales | pwntools proporcionado por quien llama, en Linux x64; GDB/pwndbg opcionales; [fallos registrados](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Módulos, importaciones, mapas de código fuente, rutas, IPC y relaciones con complementos nativos | Node.js y npm; [análisis de aplicaciones](https://rea.tools/es/guides/javascript/) |
|
||||
| Sitios web | Estructura de la página, scripts, observaciones de red y capturas de pantalla solicitadas | Un navegador de la familia Chrome; [análisis del navegador](https://rea.tools/es/guides/browser/) |
|
||||
| JavaScript / Electron | Módulos, importaciones, mapas de código fuente, rutas, IPC y relaciones con complementos nativos | Node.js y npm; [análisis de aplicaciones](../javascript-artifact-reconstruction.md) |
|
||||
| Sitios web | Estructura de la página, scripts, observaciones de red y capturas de pantalla solicitadas | Un navegador de la familia Chrome; [análisis del navegador](../browser-observation.md) |
|
||||
| Capturas de red guardadas | Solicitudes, respuestas, contenido accesible de las cargas útiles y ubicaciones de origen | HAR; mitmdump en Linux para capturas en formato nativo de mitmproxy; [guía de capturas](../web-network-captures.md) |
|
||||
| Capturas de paquetes | Endpoints, DNS, TLS SNI, distribución de protocolos y solicitudes HTTP | tshark y capinfos suministrados por el autor de la llamada; [guía pcap](../pcap-analysis.md) |
|
||||
| Ensamblados .NET | Metadatos, instrucciones CIL, dependencias nativas declaradas y comparaciones de compilaciones | Inspección estática; [guía de código administrado](../managed-code-analysis.md) |
|
||||
|
||||
@@ -124,12 +124,13 @@ REA به Node.js 22.x (>=22.19)، 24.x (>=24.11) یا 26+، بههمراه np
|
||||
|
||||
| هدف | خروجی REA | پیشنیازها و راهنما |
|
||||
| ---------------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| باینریهای بومی | شبهکد، اسمبلی، رشتهها، نمادها، فراخوانیها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](https://rea.tools/fa/guides/native/) |
|
||||
| باینریهای بومی | شبهکد، اسمبلی، رشتهها، نمادها، فراخوانیها و ارجاعات | Hopper، Ghidra یا IDA؛ [تحلیل بومی](../native-investigation.md) |
|
||||
| ساختار ELF آفلاین | بخشها، سگمنتها، نمادها و relocationهای اصلی و گزینههای احتمالی کاهش آسیبپذیری ایستا | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ [تشخیص باینری](../binary-diagnostics.md) |
|
||||
| بایتکد EVM | selectorهای dispatch، offsetهای بایتی، آرگومانهای استنباطشده و قابلیت تغییر وضعیت | دادهٔ خام/hex محلی روی Linux x64؛ [راهنمای EVM آفلاین](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| کرشهای ثبتشدهٔ لینوکس | یادداشتهای خام، رجیسترها/سیگنالهای تمام threadهای ثبتشده و گزینههای احتمالی نگاشت | pwntools تأمینشده توسط فراخواننده روی Linux x64؛ GDB/pwndbg اختیاری؛ [کرشهای ثبتشده](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | ماژولها، importها، source mapها، مسیرها، IPC و ارتباط افزونههای بومی | Node.js و npm؛ [تحلیل برنامه](https://rea.tools/fa/guides/javascript/) |
|
||||
| وبسایتها | ساختار صفحه، اسکریپتها، مشاهدات شبکه و اسکرینشاتهای درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](https://rea.tools/fa/guides/browser/) |
|
||||
| JavaScript / Electron | ماژولها، importها، source mapها، مسیرها، IPC و ارتباط افزونههای بومی | Node.js و npm؛ [تحلیل برنامه](../javascript-artifact-reconstruction.md) |
|
||||
| وبسایتها | ساختار صفحه، اسکریپتها، مشاهدات شبکه و اسکرینشاتهای درخواستی | مرورگر مبتنی بر Chrome؛ [تحلیل مرورگر](../browser-observation.md) |
|
||||
| دادههای ضبطشدهٔ شبکه | درخواستها، پاسخها، payloadهای آشکارشده و مکان آنها در منبع | HAR؛ ابزار mitmdump در لینوکس برای ضبطهای بومی mitmproxy؛ [راهنمای ضبط شبکه](../web-network-captures.md) |
|
||||
| اسمبلیهای .NET | فراداده، دستورالعملهای CIL، وابستگیهای بومی اعلامشده و مقایسهٔ buildها | بررسی ایستا؛ [راهنمای کد مدیریتشده](../managed-code-analysis.md) |
|
||||
| فایلهای APK اندروید | تعاریف Manifest، کلاسها، متدهای دیکامپایلشده و ارجاعات | JADX بدون رابط گرافیکی و JDK کامل در Linux/macOS/Windows x64؛ [راهنمای اندروید](../android-analysis.md) |
|
||||
|
||||
@@ -135,13 +135,14 @@ REA nécessite Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, ainsi que npm. Les
|
||||
|
||||
| Cible | Ce que REA renvoie | Prérequis et guide |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](https://rea.tools/fr/guides/native/) |
|
||||
| Binaires natifs | Pseudo-code, assembleur, chaînes, symboles, appels et références | Hopper, Ghidra ou IDA ; [analyse native](../native-investigation.md) |
|
||||
| Structure ELF hors ligne | Sections, segments, symboles et relocations d'origine, et protections statiques candidates | pwntools fourni par l'utilisateur sous Linux x64 ; [diagnostics binaires](../binary-diagnostics.md) |
|
||||
| Artefact WASM | Validation, sections, formes WAT exactes d'import/export et WAT décodé | WABT 1.0.42 fourni par l'utilisateur ; `inspect_wasm_artifact` |
|
||||
| Bytecode EVM | Sélecteurs de dispatch, offsets en octets, arguments inférés et mutabilité | Fichier local brut ou hexadécimal sous Linux x64 ; [guide EVM hors ligne](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Crashs Linux enregistrés | Notes brutes, registres et signaux de chaque thread enregistré, et correspondances de mappings facultatives | pwntools fourni par l'utilisateur sous Linux x64 ; GDB/pwndbg en option ; [crashs enregistrés](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](https://rea.tools/fr/guides/javascript/) |
|
||||
| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](https://rea.tools/fr/guides/browser/) |
|
||||
| JavaScript / Electron | Modules, imports, source maps, routes, IPC et relations avec les modules natifs | Node.js et npm ; [analyse d'applications](../javascript-artifact-reconstruction.md) |
|
||||
| Sites web | Structure des pages, scripts, observations réseau et captures d'écran demandées | Un navigateur de la famille Chrome ; [analyse du navigateur](../browser-observation.md) |
|
||||
| Captures réseau enregistrées | Requêtes, réponses, payloads exposés et emplacements dans le code source | HAR ; mitmdump sous Linux pour les captures mitmproxy natives ; [guide des captures](../web-network-captures.md) |
|
||||
| Captures de paquets | Points de terminaison, DNS, TLS SNI, distribution des protocoles et requêtes HTTP | tshark et capinfos fournis par l'appelant ; [guide pcap](../pcap-analysis.md) |
|
||||
| Assemblys .NET | Métadonnées, instructions CIL, dépendances natives déclarées et comparaisons de builds | Inspection statique ; [guide du code managé](../managed-code-analysis.md) |
|
||||
|
||||
@@ -138,13 +138,14 @@ Alat tambahan dan dukungan sistem host bergantung pada target yang dianalisis:
|
||||
|
||||
| Target | Hasil yang diberikan REA | Persyaratan dan panduan |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Biner native | Pseudocode, assembly, string, simbol, pemanggilan, dan referensi | Hopper, Ghidra, atau IDA; [analisis native](https://rea.tools/id/guides/native/) |
|
||||
| Biner native | Pseudocode, assembly, string, simbol, pemanggilan, dan referensi | Hopper, Ghidra, atau IDA; [analisis native](../native-investigation.md) |
|
||||
| Tata letak ELF offline | Bagian, segmen, simbol/relokasi asli, dan kandidat mitigasi statis | pwntools yang disediakan pemanggil pada Linux x64; [diagnostik biner](../binary-diagnostics.md) |
|
||||
| Artefak WASM | Validasi, bagian, bentuk WAT import/ekspor yang tepat, dan WAT yang didekode | WABT 1.0.42 yang disediakan pemanggil; `inspect_wasm_artifact` |
|
||||
| Bytecode EVM | Selector dispatch, offset byte, argumen yang disimpulkan, dan sifat mutabilitas | Pembawa data raw/hex lokal; [panduan EVM offline](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Crash Linux yang direkam | Catatan mentah, register/sinyal setiap thread yang direkam, dan kandidat pemetaan opsional | pwntools yang disediakan pemanggil; GDB/pwndbg opsional; [crash yang direkam](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](https://rea.tools/id/guides/javascript/) |
|
||||
| Situs web | Struktur halaman, skrip, pengamatan jaringan, dan tangkapan layar yang diminta | Peramban berbasis Chrome; [analisis peramban](https://rea.tools/id/guides/browser/) |
|
||||
| JavaScript / Electron | Modul, impor, source map, rute, IPC, dan hubungan add-on native | Node.js dan npm; [analisis aplikasi](../javascript-artifact-reconstruction.md) |
|
||||
| Situs web | Struktur halaman, skrip, pengamatan jaringan, dan tangkapan layar yang diminta | Peramban berbasis Chrome; [analisis peramban](../browser-observation.md) |
|
||||
| Rekaman jaringan tersimpan | Permintaan, respons, payload yang terekspos, dan lokasi sumber | HAR; mitmdump di Linux untuk rekaman native mitmproxy; [panduan rekaman](../web-network-captures.md) |
|
||||
| Capturan paket | Endpoint, DNS, TLS SNI, distribusi protokol, dan permintaan HTTP | tshark dan capinfos buatan pemanggil; [panduan pcap](../pcap-analysis.md) |
|
||||
| Assembly .NET | Metadata, instruksi CIL, dependensi native yang dideklarasikan, dan perbandingan build | Tidak ada (hanya analisis statis); [panduan kode terkelola](../managed-code-analysis.md) |
|
||||
|
||||
@@ -135,13 +135,14 @@ REA richiede Node.js 22.x (>=22.19), 24.x (>=24.11) o 26+, oltre a npm. Strument
|
||||
|
||||
| Obiettivo | Cosa restituisce REA | Requisiti e guida |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Binari nativi | Pseudocodice, assembly, stringhe, simboli, chiamate e riferimenti | Hopper, Ghidra o IDA; [analisi nativa](https://rea.tools/it/guides/native/) |
|
||||
| Binari nativi | Pseudocodice, assembly, stringhe, simboli, chiamate e riferimenti | Hopper, Ghidra o IDA; [analisi nativa](../native-investigation.md) |
|
||||
| Layout ELF offline | Sezioni, segmenti, simboli/relocazioni originali e candidati mitigazioni statiche | pwntools fornito dal chiamante su Linux x64; [diagnostica binaria](../binary-diagnostics.md) |
|
||||
| Artefatto WASM | Convalida, sezioni, forme WAT di import/export esatte e WAT decodificato | WABT 1.0.42 fornito dal chiamante; `inspect_wasm_artifact` |
|
||||
| Bytecode EVM | Selettori di dispatch, offset dei byte, argomenti inferiti e mutabilità | Carrier raw/hex locale; [guida EVM offline](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Crash Linux registrati | Note grezze, registri/segnai di ogni thread registrato e candidati mapping opzionali | pwntools fornito dal chiamante; GDB/pwndbg opzionale; [crash registrati](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](https://rea.tools/it/guides/javascript/) |
|
||||
| Siti web | Struttura della pagina, script, osservazioni di rete e screenshot richiesti | Un browser della famiglia Chrome; [analisi browser](https://rea.tools/it/guides/browser/) |
|
||||
| JavaScript / Electron | Moduli, import, source map, route, IPC e relazioni con add-on nativi | Node.js e npm; [analisi delle applicazioni](../javascript-artifact-reconstruction.md) |
|
||||
| Siti web | Struttura della pagina, script, osservazioni di rete e screenshot richiesti | Un browser della famiglia Chrome; [analisi browser](../browser-observation.md) |
|
||||
| Acquisizioni di rete salvate | Richieste, risposte, payload esposti e posizioni sorgente | HAR; mitmdump su Linux per acquisizioni mitmproxy native; [guida alle acquisizioni](../web-network-captures.md) |
|
||||
| Captured di pacchetti | Endpoint, DNS, TLS SNI, distribuzione dei protocolli e richieste HTTP | tshark e capinfos forniti dal chiamante; [guida pcap](../pcap-analysis.md) |
|
||||
| Assembly .NET | Metadati, istruzioni CIL, dipendenze native dichiarate e confronti di build | Nessuno (solo statico); [guida managed-code](../managed-code-analysis.md) |
|
||||
|
||||
@@ -132,12 +132,13 @@ REA には Node.js 22.x(>=22.19)、24.x(>=24.11)、または 26+ と npm
|
||||
|
||||
| 対象 | REA が返すもの | 要件とガイド |
|
||||
| -------------------------------- | ------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](https://rea.tools/ja/guides/native/) |
|
||||
| ネイティブバイナリ | 疑似コード、アセンブリ、文字列、シンボル、呼び出し、参照 | Hopper、Ghidra、または IDA;[ネイティブ分析](../native-investigation.md) |
|
||||
| オフライン ELF レイアウト | セクション、セグメント、元のシンボル/再配置情報、静的解析によるセキュリティ対策の候補 | Linux x64 で利用者が用意する pwntools;[バイナリ診断](../binary-diagnostics.md) |
|
||||
| EVM バイトコード | ディスパッチセレクター、バイトオフセット、推定された引数と状態変更の可否 | Linux x64 上のローカルの生バイト/16 進数の入力ファイル;[オフライン EVM ガイド](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| 記録済みの Linux クラッシュ | 生の note レコード、記録された全スレッドのレジスター/シグナル、任意のマッピング候補 | Linux x64 で利用者が用意する pwntools;GDB/pwndbg は任意;[記録済みクラッシュ](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](https://rea.tools/ja/guides/javascript/) |
|
||||
| ウェブサイト | ページ構造、スクリプト、ネットワークの観測結果、依頼されたスクリーンショット | Chrome 系ブラウザー;[ブラウザー分析](https://rea.tools/ja/guides/browser/) |
|
||||
| JavaScript / Electron | モジュール、インポート、ソースマップ、ルート、IPC、ネイティブアドオンの関係 | Node.js と npm;[アプリケーション分析](../javascript-artifact-reconstruction.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) |
|
||||
|
||||
@@ -132,12 +132,13 @@ REA에는 Node.js 22.x(>=22.19), 24.x(>=24.11) 또는 26+와 npm이 필요합니
|
||||
|
||||
| 대상 | REA가 반환하는 내용 | 요구 사항 및 가이드 |
|
||||
| --------------------- | ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](https://rea.tools/ko/guides/native/) |
|
||||
| 네이티브 바이너리 | 의사 코드, 어셈블리, 문자열, 심볼, 호출, 참조 | Hopper, Ghidra 또는 IDA; [네이티브 분석](../native-investigation.md) |
|
||||
| 오프라인 ELF 레이아웃 | 섹션, 세그먼트, 원본 심볼/재배치 정보, 정적 분석으로 얻은 보안 완화 기법 후보 | Linux x64에서 호출자가 제공하는 pwntools; [바이너리 진단](../binary-diagnostics.md) |
|
||||
| EVM 바이트코드 | 디스패치 선택자, 바이트 오프셋, 추론한 인자와 상태 변경 가능 여부 | Linux x64의 로컬 원시 바이트/16진수 입력 파일; [오프라인 EVM 가이드](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| 기록된 Linux 크래시 | 원시 note 레코드, 기록된 모든 스레드의 레지스터/시그널, 선택적 매핑 후보 | Linux x64에서 호출자가 제공하는 pwntools; GDB/pwndbg는 선택 사항; [기록된 크래시](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](https://rea.tools/ko/guides/javascript/) |
|
||||
| 웹사이트 | 페이지 구조, 스크립트, 네트워크 관찰 결과, 요청한 스크린샷 | Chrome 계열 브라우저; [브라우저 분석](https://rea.tools/ko/guides/browser/) |
|
||||
| JavaScript / Electron | 모듈, 임포트, 소스 맵, 라우트, IPC, 네이티브 애드온 관계 | Node.js와 npm; [애플리케이션 분석](../javascript-artifact-reconstruction.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) |
|
||||
|
||||
@@ -132,12 +132,13 @@ REA wymaga Node.js 22.x (>=22.19), 24.x (>=24.11) lub 26+ oraz npm. Dodatkowe na
|
||||
|
||||
| Cel | Co zwraca REA | Wymagania i przewodnik |
|
||||
| --------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](https://rea.tools/pl/guides/native/) |
|
||||
| Natywne pliki binarne | Pseudokod, kod asemblera, ciągi znaków, symbole, wywołania i odwołania | Hopper, Ghidra lub IDA; [analiza kodu natywnego](../native-investigation.md) |
|
||||
| Układ pliku ELF w trybie offline | Sekcje, segmenty, oryginalne symbole/relokacje i potencjalne mechanizmy zabezpieczeń wskazane przez analizę statyczną | pwntools dostarczone przez użytkownika w systemie Linux x64; [diagnostyka plików binarnych](../binary-diagnostics.md) |
|
||||
| Kod bajtowy EVM | Selektory wywołań, przesunięcia bajtowe, wywnioskowane argumenty i mutowalność | Lokalny plik z surowymi bajtami lub zapisem szesnastkowym w systemie Linux x64; [przewodnik EVM offline](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Zarejestrowane awarie systemu Linux | Surowe rekordy note, rejestry/sygnały każdego zarejestrowanego wątku i opcjonalni kandydaci na mapowania | pwntools dostarczone przez użytkownika w systemie Linux x64; opcjonalnie GDB/pwndbg; [zarejestrowane awarie](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](https://rea.tools/pl/guides/javascript/) |
|
||||
| Strony internetowe | Struktura strony, skrypty, obserwacje sieciowe i zamówione zrzuty ekranu | Przeglądarka z rodziny Chrome; [analiza w przeglądarce](https://rea.tools/pl/guides/browser/) |
|
||||
| JavaScript / Electron | Moduły, importy, mapy źródeł, trasy, IPC i powiązania z natywnymi dodatkami | Node.js i npm; [analiza aplikacji](../javascript-artifact-reconstruction.md) |
|
||||
| Strony internetowe | Struktura strony, skrypty, obserwacje sieciowe i zamówione zrzuty ekranu | Przeglądarka z rodziny Chrome; [analiza w przeglądarce](../browser-observation.md) |
|
||||
| Zapisane przechwycenia ruchu sieciowego | Żądania, odpowiedzi, dostępne dane przesyłane i lokalizacje źródłowe | HAR; mitmdump w systemie Linux dla natywnych przechwyceń mitmproxy; [przewodnik przechwytywania](../web-network-captures.md) |
|
||||
| Przechwytywanie pakietów | Punkty końcowe, DNS, TLS SNI, dystrybucja protokołów i żądania HTTP | Dostarczone przez wywołującego tshark i capinfos; [przewodnik pcap](../pcap-analysis.md) |
|
||||
| Zestawy .NET | Metadane, instrukcje CIL, zadeklarowane zależności natywne i porównania kompilacji | Analiza statyczna; [przewodnik kodu zarządzanego](../managed-code-analysis.md) |
|
||||
|
||||
@@ -132,12 +132,13 @@ O REA requer Node.js 22.x (>=22.19), 24.x (>=24.11) ou 26+, além do npm. As fer
|
||||
|
||||
| Alvo | O que o REA retorna | Requisitos e guia |
|
||||
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](https://rea.tools/pt-BR/guides/native/) |
|
||||
| Binários nativos | Pseudocódigo, assembly, strings, símbolos, chamadas e referências | Hopper, Ghidra ou IDA; [análise nativa](../native-investigation.md) |
|
||||
| Estrutura ELF sem execução | Seções, segmentos, símbolos/relocações originais e possíveis mecanismos de mitigação identificados por análise estática | pwntools fornecido pelo chamador no Linux x64; [diagnóstico de binários](../binary-diagnostics.md) |
|
||||
| Bytecode EVM | Seletores de despacho, offsets de bytes, argumentos inferidos e mutabilidade | Entrada local com bytes brutos ou representação hexadecimal no Linux x64; [guia de EVM sem execução](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Falhas do Linux registradas | Registros note brutos, registradores/sinais de cada thread registrada e candidatos opcionais a mapeamentos | pwntools fornecido pelo chamador no Linux x64; GDB/pwndbg opcionais; [falhas registradas](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Módulos, importações, source maps, rotas, IPC e relações com complementos nativos | Node.js e npm; [análise de aplicativos](https://rea.tools/pt-BR/guides/javascript/) |
|
||||
| Sites | Estrutura da página, scripts, observações de rede e capturas de tela solicitadas | Um navegador da família Chrome; [análise do navegador](https://rea.tools/pt-BR/guides/browser/) |
|
||||
| JavaScript / Electron | Módulos, importações, source maps, rotas, IPC e relações com complementos nativos | Node.js e npm; [análise de aplicativos](../javascript-artifact-reconstruction.md) |
|
||||
| Sites | Estrutura da página, scripts, observações de rede e capturas de tela solicitadas | Um navegador da família Chrome; [análise do navegador](../browser-observation.md) |
|
||||
| Capturas de rede salvas | Requisições, respostas, payloads acessíveis e localizações de origem | HAR; mitmdump no Linux para capturas no formato nativo do mitmproxy; [guia de capturas](../web-network-captures.md) |
|
||||
| Capturas de pacotes | Endpoints, DNS, TLS SNI, distribuição de protocolos e requisições HTTP | tshark e capinfos fornecidos pelo chamador; [guia pcap](../pcap-analysis.md) |
|
||||
| Assemblies .NET | Metadados, instruções CIL, dependências nativas declaradas e comparações de builds | Inspeção estática; [guia de código gerenciado](../managed-code-analysis.md) |
|
||||
|
||||
@@ -132,12 +132,13 @@ REA требует Node.js 22.x (>=22.19), 24.x (>=24.11) или 26+, а так
|
||||
|
||||
| Цель | Что возвращает REA | Требования и руководство |
|
||||
| ----------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Нативные бинарные файлы | Псевдокод, ассемблер, строки, символы, вызовы и ссылки | Hopper, Ghidra или IDA; [нативный анализ](https://rea.tools/ru/guides/native/) |
|
||||
| Нативные бинарные файлы | Псевдокод, ассемблер, строки, символы, вызовы и ссылки | Hopper, Ghidra или IDA; [нативный анализ](../native-investigation.md) |
|
||||
| Структура ELF без запуска | Секции, сегменты, исходные символы/релокации и возможные механизмы защиты по данным статического анализа | pwntools, предоставленный вызывающей стороной, в Linux x64; [диагностика бинарных файлов](../binary-diagnostics.md) |
|
||||
| Байт-код EVM | Селекторы диспетчеризации, смещения байтов, предполагаемые аргументы и изменяемость состояния | Локальные необработанные байты или шестнадцатеричное представление в Linux x64; [руководство по EVM без запуска](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Записанные сбои Linux | Необработанные записи note, регистры/сигналы каждого записанного потока и необязательные кандидаты сопоставления с памятью | pwntools, предоставленный вызывающей стороной, в Linux x64; необязательные GDB/pwndbg; [записанные сбои](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](https://rea.tools/ru/guides/javascript/) |
|
||||
| Сайты | Структура страницы, скрипты, наблюдения за сетью и запрошенные снимки экрана | Браузер семейства Chrome; [анализ браузера](https://rea.tools/ru/guides/browser/) |
|
||||
| JavaScript / Electron | Модули, импорты, карты исходного кода, маршруты, IPC и связи с нативными дополнениями | Node.js и npm; [анализ приложений](../javascript-artifact-reconstruction.md) |
|
||||
| Сайты | Структура страницы, скрипты, наблюдения за сетью и запрошенные снимки экрана | Браузер семейства Chrome; [анализ браузера](../browser-observation.md) |
|
||||
| Сохранённые сетевые захваты | Запросы, ответы, доступные полезные данные и позиции в источнике | HAR; mitmdump в Linux для захватов в собственном формате mitmproxy; [руководство по сетевым захватам](../web-network-captures.md) |
|
||||
| Сборки .NET | Метаданные, инструкции CIL, объявленные нативные зависимости и сравнения сборок | Статическое исследование; [руководство по управляемому коду](../managed-code-analysis.md) |
|
||||
| Android APK | Объявления манифеста, классы, декомпилированные методы и ссылки | JADX без графического интерфейса и полный JDK в Linux/macOS/Windows x64; [руководство по Android](../android-analysis.md) |
|
||||
|
||||
@@ -132,12 +132,13 @@ REA ต้องใช้ Node.js 22.x (>=22.19), 24.x (>=24.11) หรือ 26
|
||||
|
||||
| เป้าหมาย | สิ่งที่ REA ส่งกลับ | ข้อกำหนดและคู่มือ |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
||||
| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](https://rea.tools/th/guides/native/) |
|
||||
| ไบนารีเนทีฟ | โค้ดเทียม แอสเซมบลี สตริง สัญลักษณ์ การเรียก และการอ้างอิง | Hopper, Ghidra หรือ IDA; [การวิเคราะห์เนทีฟ](../native-investigation.md) |
|
||||
| โครงสร้าง ELF แบบออฟไลน์ | เซกชัน เซกเมนต์ สัญลักษณ์/รีโลเคชันต้นฉบับ และตัวเลือกกลไกป้องกันที่พบจากการวิเคราะห์แบบสถิต | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; [การวินิจฉัยไบนารี](../binary-diagnostics.md) |
|
||||
| ไบต์โค้ด EVM | ตัวเลือกฟังก์ชันสำหรับการเรียก ออฟเซ็ตไบต์ อาร์กิวเมนต์และความสามารถในการเปลี่ยนสถานะที่อนุมานได้ | ไฟล์บนเครื่องที่มีไบต์ดิบหรือข้อมูลเลขฐานสิบหกบน Linux x64; [คู่มือ EVM แบบออฟไลน์](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| ข้อมูลการแครชของ Linux ที่บันทึกไว้ | ระเบียน note ดิบ รีจิสเตอร์/สัญญาณของทุกเธรดที่บันทึกไว้ และตัวเลือกการแมปเพิ่มเติม | pwntools ที่ผู้ใช้จัดเตรียมบน Linux x64; เลือกใช้ GDB/pwndbg เพิ่มเติมได้; [การแครชที่บันทึกไว้](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](https://rea.tools/th/guides/javascript/) |
|
||||
| เว็บไซต์ | โครงสร้างหน้า สคริปต์ ข้อมูลเครือข่ายที่สังเกตได้ และภาพหน้าจอที่ร้องขอ | เบราว์เซอร์ในตระกูล Chrome; [การวิเคราะห์ผ่านเบราว์เซอร์](https://rea.tools/th/guides/browser/) |
|
||||
| JavaScript / Electron | โมดูล การนำเข้า ซอร์สแมป เส้นทาง IPC และความสัมพันธ์กับส่วนเสริมเนทีฟ | Node.js และ npm; [การวิเคราะห์แอปพลิเคชัน](../javascript-artifact-reconstruction.md) |
|
||||
| เว็บไซต์ | โครงสร้างหน้า สคริปต์ ข้อมูลเครือข่ายที่สังเกตได้ และภาพหน้าจอที่ร้องขอ | เบราว์เซอร์ในตระกูล Chrome; [การวิเคราะห์ผ่านเบราว์เซอร์](../browser-observation.md) |
|
||||
| ข้อมูลเครือข่ายที่บันทึกไว้ | คำขอ คำตอบ เพย์โหลดที่ปรากฏ และตำแหน่งต้นทาง | HAR; mitmdump บน Linux สำหรับข้อมูลที่บันทึกในรูปแบบเนทีฟของ mitmproxy; [คู่มือการบันทึกข้อมูล](../web-network-captures.md) |
|
||||
| การจับแพ็กเก็ต | จุดเชื่อมต่อ, DNS, TLS SNI, การกระจายโปรโตคอล และคำขอ HTTP | tshark และ capinfos ที่ผู้เรียกจัดเตรียม; [คู่มือ pcap](../pcap-analysis.md) |
|
||||
| แอสเซมบลี .NET | เมทาดาทา คำสั่ง CIL การพึ่งพาเนทีฟที่ประกาศไว้ และการเปรียบเทียบผลการบิลด์ | การตรวจสอบแบบสถิต; [คู่มือโค้ดแบบจัดการ](../managed-code-analysis.md) |
|
||||
|
||||
+23
-23
@@ -132,30 +132,30 @@ Ajanınız, hedefi incelemek ve ilgili kodu izlemek için MCP üzerinden REA'yı
|
||||
|
||||
REA; Node.js 22.x (>=22.19), 24.x (>=24.11) veya 26+ ve npm gerektirir. Ek araçlar ve desteklenen ana bilgisayar sistemleri hedefe bağlıdır:
|
||||
|
||||
| Hedef | REA'nın döndürdükleri | Gereksinimler ve rehber |
|
||||
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Yerel makine kodu içeren ikili dosyalar | Sözde kod, assembly kodu, dizeler, semboller, çağrılar ve referanslar | Hopper, Ghidra veya IDA; [yerel kod analizi](https://rea.tools/tr/guides/native/) |
|
||||
| Ç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 |
|
||||
| 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+ |
|
||||
| Kaydedilmiş Linux çökmeleri | Ham note kayıtları, kaydedilen her iş parçacığının yazmaçları/sinyalleri ve isteğe bağlı eşleme adayları | Linux x64 üzerinde çağıranın sağladığı pwntools; isteğe bağlı GDB/pwndbg; [kaydedilmiş çökmeler](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modüller, içe aktarmalar, kaynak haritaları, rotalar, IPC ve yerel eklenti ilişkileri | Node.js ve npm; [uygulama analizi](https://rea.tools/tr/guides/javascript/) |
|
||||
| Web siteleri | Sayfa yapısı, betikler, ağ gözlemleri ve istenen ekran görüntüleri | Chrome ailesinden bir tarayıcı; [tarayıcı analizi](https://rea.tools/tr/guides/browser/) |
|
||||
| Kaydedilmiş ağ yakalamaları | İstekler, yanıtlar, erişilebilir yük içerikleri ve kaynak konumları | HAR veya Fiddler Everywhere SAZ; Linux'ta native mitmproxy için mitmdump; [ağ yakalama rehberi](../web-network-captures.md) |
|
||||
| .NET derlemeleri | Meta veriler, CIL talimatları, bildirilmiş yerel bağımlılıklar ve derleme karşılaştırmaları | Statik inceleme; [yönetilen kod rehberi](../managed-code-analysis.md) |
|
||||
| Android APK'leri | Manifest bildirimleri, sınıflar, tersine derlenmiş metotlar ve referanslar | Linux/macOS/Windows x64 üzerinde arayüzsüz JADX ve tam bir JDK; [Android rehberi](../android-analysis.md) |
|
||||
| Android cihazları | Cihazlar, paketler, süreçler, günlükler, dökümler, ekran görüntüleri ve APK/dosya aktarımları | Çağıranın sağladığı adb (emülatör veya cihaz); [ADB rehberi](../adb-device-analysis.md) |
|
||||
| JEB projeleri | Motor destekli proje birimleri, artefakt özetleri ve tür/metot sözdekodu | Çağıranın başlattığı, MCP sunan bir JEB istemcisi, REA_JEB_MCP_URL ile seçilir; [JEB rehberi](../jeb-analysis.md) |
|
||||
| Android kaynakları | Çözülmüş manifest, dize tabloları, diller ve sürüm bilgileri | Çağıranın sağladığı Apktool başlatıcısı; [kaynak rehberi](../apktool-resource-analysis.md) |
|
||||
| Flutter uygulamaları | Dart anlık görüntü karması, ABI başına kitaplık özetleri ve motor derleme kökeni | Yok (statik APK çözümleme); [Flutter rehberi](../flutter-analysis.md) |
|
||||
| Donanım yazılımı | Bölgeler, çıkarma sonuçları ve yerel kod analizine devirler | Linux üzerinde Binwalk / Unblob; [donanım yazılımı rehberi](../firmware-analysis.md) |
|
||||
| Kaydedilmiş paketler | Seçilen alanlar, yinelenen değerler, filtreler ve çözümleme kuralları | Linux: TShark 4.x + prlimit; [yakalama kılavuzu](../packet-captures.md) |
|
||||
| Paket özetleri | Uç noktalar, DNS, TLS SNI, protokol dağılımı ve HTTP istekleri | Çağıranın sağladığı tshark ve capinfos (Linux üzerinde doğrulandı); [pcap rehberi](../pcap-analysis.md) |
|
||||
| Kaydedilmiş mantık sinyalleri | Kod çözücü açıklamaları, örnek aralıkları ve tanılama | Linux: sigrok-cli 0.7.2 + prlimit; [sinyal kılavuzu](../signal-recordings.md) |
|
||||
| Hedef | REA'nın döndürdükleri | Gereksinimler ve rehber |
|
||||
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Yerel makine kodu içeren ikili dosyalar | Sözde kod, assembly kodu, dizeler, semboller, çağrılar ve referanslar | Hopper, Ghidra veya IDA; [yerel kod analizi](../native-investigation.md) |
|
||||
| Çevrimdışı ELF yapısı | Bölümler, segmentler, özgün semboller/yeniden konumlandırmalar ve statik analizden olası güvenlik önlemleri | Linux x64 üzerinde çağıranın sağladığı pwntools; [ikili dosya tanılama](../binary-diagnostics.md) |
|
||||
| Go ikili dosyaları | Gömülü compiler sürümü, modüller, replacement'lar ve build ayarları | Yerleşik statik ELF/PE/thin Mach-O incelemesi; Go kurulumu gerekmez |
|
||||
| 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+ |
|
||||
| Kaydedilmiş Linux çökmeleri | Ham note kayıtları, kaydedilen her iş parçacığının yazmaçları/sinyalleri ve isteğe bağlı eşleme adayları | Linux x64 üzerinde çağıranın sağladığı pwntools; isteğe bağlı GDB/pwndbg; [kaydedilmiş çökmeler](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Modüller, içe aktarmalar, kaynak haritaları, rotalar, IPC ve yerel eklenti ilişkileri | Node.js ve npm; [uygulama analizi](../javascript-artifact-reconstruction.md) |
|
||||
| Web siteleri | Sayfa yapısı, betikler, ağ gözlemleri ve istenen ekran görüntüleri | Chrome ailesinden bir tarayıcı; [tarayıcı analizi](../browser-observation.md) |
|
||||
| Kaydedilmiş ağ yakalamaları | İstekler, yanıtlar, erişilebilir yük içerikleri ve kaynak konumları | HAR veya Fiddler Everywhere SAZ; Linux'ta native mitmproxy için mitmdump; [ağ yakalama rehberi](../web-network-captures.md) |
|
||||
| .NET derlemeleri | Meta veriler, CIL talimatları, bildirilmiş yerel bağımlılıklar ve derleme karşılaştırmaları | Statik inceleme; [yönetilen kod rehberi](../managed-code-analysis.md) |
|
||||
| Android APK'leri | Manifest bildirimleri, sınıflar, tersine derlenmiş metotlar ve referanslar | Linux/macOS/Windows x64 üzerinde arayüzsüz JADX ve tam bir JDK; [Android rehberi](../android-analysis.md) |
|
||||
| Android cihazları | Cihazlar, paketler, süreçler, günlükler, dökümler, ekran görüntüleri ve APK/dosya aktarımları | Çağıranın sağladığı adb (emülatör veya cihaz); [ADB rehberi](../adb-device-analysis.md) |
|
||||
| JEB projeleri | Motor destekli proje birimleri, artefakt özetleri ve tür/metot sözdekodu | Çağıranın başlattığı, MCP sunan bir JEB istemcisi, REA_JEB_MCP_URL ile seçilir; [JEB rehberi](../jeb-analysis.md) |
|
||||
| Android kaynakları | Çözülmüş manifest, dize tabloları, diller ve sürüm bilgileri | Çağıranın sağladığı Apktool başlatıcısı; [kaynak rehberi](../apktool-resource-analysis.md) |
|
||||
| Flutter uygulamaları | Dart anlık görüntü karması, ABI başına kitaplık özetleri ve motor derleme kökeni | Yok (statik APK çözümleme); [Flutter rehberi](../flutter-analysis.md) |
|
||||
| Donanım yazılımı | Bölgeler, çıkarma sonuçları ve yerel kod analizine devirler | Linux üzerinde Binwalk / Unblob; [donanım yazılımı rehberi](../firmware-analysis.md) |
|
||||
| Kaydedilmiş paketler | Seçilen alanlar, yinelenen değerler, filtreler ve çözümleme kuralları | Linux: TShark 4.x + prlimit; [yakalama kılavuzu](../packet-captures.md) |
|
||||
| Paket özetleri | Uç noktalar, DNS, TLS SNI, protokol dağılımı ve HTTP istekleri | Çağıranın sağladığı tshark ve capinfos (Linux üzerinde doğrulandı); [pcap rehberi](../pcap-analysis.md) |
|
||||
| Kaydedilmiş mantık sinyalleri | Kod çözücü açıklamaları, örnek aralıkları ve tanılama | Linux: sigrok-cli 0.7.2 + prlimit; [sinyal kılavuzu](../signal-recordings.md) |
|
||||
| Paketler ve kaynaklar | Dosya envanterleri, özet değerleri, plist'ler, Apple paket yapısı ve çıkarılmış kaynaklar | [Artefakt ve JavaScript rehberi](../javascript-artifact-reconstruction.md), [Apple uygulamaları](../apple-application-analysis.md) |
|
||||
| Süreç davranışı | Terminal çıktısı, etkileşimler, sonlanma ve dosya sistemi gözlemleri, çalıştırma karşılaştırmaları | Yerel PTY ile Linux/macOS; [süreç yakalama](../process-capture.md) |
|
||||
| Süreç davranışı | Terminal çıktısı, etkileşimler, sonlanma ve dosya sistemi gözlemleri, çalıştırma karşılaştırmaları | Yerel PTY ile Linux/macOS; [süreç yakalama](../process-capture.md) |
|
||||
|
||||
Statik JavaScript ve .NET incelemesi, uygulamayı çalıştırmadan sağlanan dosyaları okur. Çalışma zamanı yakalaması, seçilen hedefi kullanıcı izinlerinizle çalıştırır veya onunla etkileşime girer; her çalışma zamanı rehberi etkilerini açıklar.
|
||||
|
||||
|
||||
@@ -132,12 +132,13 @@ REA потребує Node.js 22.x (>=22.19), 24.x (>=24.11) або 26+, а та
|
||||
|
||||
| Ціль | Що повертає REA | Вимоги й посібник |
|
||||
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](https://rea.tools/uk/guides/native/) |
|
||||
| Нативні бінарні файли | Псевдокод, асемблерний код, рядки, символи, виклики й посилання | Hopper, Ghidra або IDA; [нативний аналіз](../native-investigation.md) |
|
||||
| Структура ELF без запуску | Секції, сегменти, початкові символи/релокації та можливі захисні механізми за даними статичного аналізу | pwntools, наданий стороною, що викликає інструмент, у Linux x64; [діагностика бінарних файлів](../binary-diagnostics.md) |
|
||||
| Байт-код EVM | Селектори диспетчеризації, зміщення байтів, виведені припущення про аргументи та змінюваність стану | Локальні необроблені байти або шістнадцяткове представлення у Linux x64; [посібник EVM без запуску](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Записані аварійні завершення в Linux | Необроблені записи note, регістри/сигнали кожного записаного потоку й необов'язкові кандидати зіставлення пам'яті | pwntools, наданий стороною, що викликає інструмент, у Linux x64; необов'язкові GDB/pwndbg; [записані аварійні завершення](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](https://rea.tools/uk/guides/javascript/) |
|
||||
| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](https://rea.tools/uk/guides/browser/) |
|
||||
| JavaScript / Electron | Модулі, імпорти, карти вихідного коду, маршрути, IPC і зв'язки з нативними доповненнями | Node.js і npm; [аналіз застосунків](../javascript-artifact-reconstruction.md) |
|
||||
| Сайти | Структура сторінки, скрипти, мережеві спостереження й запитані знімки екрана | Браузер сімейства Chrome; [аналіз браузера](../browser-observation.md) |
|
||||
| Збережені мережеві захоплення | Запити, відповіді, доступні корисні дані та місця в джерелі | HAR; mitmdump у Linux для захоплень у власному форматі mitmproxy; [посібник мережевих захоплень](../web-network-captures.md) |
|
||||
| Захоплення пакетів | Кінцеві точки, DNS, TLS SNI, розподіл протоколів та HTTP-запити | Надані викликаючою стороною tshark та capinfos; [підручник pcap](../pcap-analysis.md) |
|
||||
| Збірки .NET | Метадані, інструкції CIL, оголошені нативні залежності й порівняння збірок | Статична перевірка; [посібник керованого коду](../managed-code-analysis.md) |
|
||||
|
||||
@@ -132,12 +132,13 @@ REA yêu cầu Node.js 22.x (>=22.19), 24.x (>=24.11) hoặc 26+, cùng với np
|
||||
|
||||
| Mục tiêu | REA trả về gì | Yêu cầu và hướng dẫn |
|
||||
| ----------------------------- | --------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Tệp nhị phân mã máy | Mã giả, hợp ngữ, chuỗi, ký hiệu, lời gọi và tham chiếu | Hopper, Ghidra hoặc IDA; [phân tích mã máy](https://rea.tools/vi/guides/native/) |
|
||||
| Tệp nhị phân mã máy | Mã giả, hợp ngữ, chuỗi, ký hiệu, lời gọi và tham chiếu | Hopper, Ghidra hoặc IDA; [phân tích mã máy](../native-investigation.md) |
|
||||
| Bố cục ELF ngoại tuyến | Các section, segment, ký hiệu/thông tin tái định vị gốc và các biện pháp giảm thiểu có thể có từ phân tích tĩnh | pwntools do bên gọi cung cấp trên Linux x64; [chẩn đoán tệp nhị phân](../binary-diagnostics.md) |
|
||||
| Bytecode EVM | Bộ chọn điều phối, độ lệch byte, tham số suy luận và khả năng thay đổi trạng thái | Đầu vào cục bộ chứa byte thô hoặc dạng thập lục phân trên Linux x64; [hướng dẫn EVM ngoại tuyến](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| Sự cố Linux đã ghi lại | Bản ghi note thô, thanh ghi/tín hiệu của mọi luồng được ghi lại và các ứng viên ánh xạ tùy chọn | pwntools do bên gọi cung cấp trên Linux x64; GDB/pwndbg tùy chọn; [sự cố đã ghi lại](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](https://rea.tools/vi/guides/javascript/) |
|
||||
| Trang web | Cấu trúc trang, script, quan sát mạng và ảnh chụp màn hình được yêu cầu | Trình duyệt thuộc họ Chrome; [phân tích trình duyệt](https://rea.tools/vi/guides/browser/) |
|
||||
| JavaScript / Electron | Mô-đun, các khai báo import, source map, tuyến, IPC và quan hệ với tiện ích mở rộng mã máy | Node.js và npm; [phân tích ứng dụng](../javascript-artifact-reconstruction.md) |
|
||||
| Trang web | Cấu trúc trang, script, quan sát mạng và ảnh chụp màn hình được yêu cầu | Trình duyệt thuộc họ Chrome; [phân tích trình duyệt](../browser-observation.md) |
|
||||
| Bản ghi lưu lượng mạng đã lưu | Yêu cầu, phản hồi, payload có thể truy cập và vị trí nguồn | HAR; mitmdump trên Linux cho bản ghi định dạng gốc của mitmproxy; [hướng dẫn bản ghi mạng](../web-network-captures.md) |
|
||||
| Bắt gói tin | Điểm cuối, DNS, TLS SNI, phân bố giao thức và yêu cầu HTTP | tshark và capinfos do nơi gọi cung cấp; [hướng dẫn pcap](../pcap-analysis.md) |
|
||||
| Assembly .NET | Siêu dữ liệu, lệnh CIL, các phụ thuộc mã máy được khai báo và so sánh bản dựng | Kiểm tra tĩnh; [hướng dẫn mã được quản lý](../managed-code-analysis.md) |
|
||||
|
||||
@@ -134,14 +134,15 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
|
||||
|
||||
| 目標 | REA 返回的內容 | 要求與指南 |
|
||||
| --------------------- | --------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/zh-TW/guides/native/) |
|
||||
| 原生二進位檔案 | 偽程式碼、組合語言、字串、符號、呼叫和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
|
||||
| 離線 ELF 佈局 | 節、段、原始符號/重定位資訊及靜態防護機制候選項 | Linux x64 上由呼叫方提供的 pwntools;[二進位診斷](../binary-diagnostics.md) |
|
||||
| Go 二進位檔案 | 內嵌編譯器版本、模組、替換項和建置設定 | 內建靜態 ELF/PE/thin Mach-O 檢查;無需安裝 Go |
|
||||
| WASM 工件 | 驗證、節、精確的匯入/匯出 WAT 形式及解碼後的 WAT | 呼叫方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
|
||||
| EVM 位元組碼 | 分派選擇器、位元組偏移、推斷的參數和狀態可變性 | Linux x64 上的本地原始位元組/十六進位輸入載體;[離線 EVM 指南](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| 已記錄的 Linux 崩潰 | 原始 note 記錄、每個已記錄執行緒的暫存器/訊號及可選的對映候選項 | Linux x64 上由呼叫方提供的 pwntools;可選的 GDB/pwndbg;[已記錄崩潰](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](https://rea.tools/zh-TW/guides/javascript/) |
|
||||
| 網站 | 頁面結構、腳本、網路觀察結果和按請求獲取的螢幕擷取畫面 | Chrome 系瀏覽器;[瀏覽器分析](https://rea.tools/zh-TW/guides/browser/) |
|
||||
| JavaScript / Electron | 模組、匯入、source map、路由、IPC 和原生附加元件關係 | Node.js 和 npm;[應用分析](../javascript-artifact-reconstruction.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) |
|
||||
|
||||
@@ -134,14 +134,15 @@ REA 需要 Node.js 22.x(>=22.19)、24.x(>=24.11)或 26+,以及 npm。
|
||||
|
||||
| 目标 | REA 返回的内容 | 要求与指南 |
|
||||
| --------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](https://rea.tools/zh-CN/guides/native/) |
|
||||
| 原生二进制文件 | 伪代码、汇编、字符串、符号、调用和引用 | Hopper、Ghidra 或 IDA;[原生分析](../native-investigation.md) |
|
||||
| 离线 ELF 布局 | 节、段、原始符号/重定位信息及静态防护机制候选项 | Linux x64 上由调用方提供的 pwntools;[二进制诊断](../binary-diagnostics.md) |
|
||||
| Go 二进制文件 | 内嵌编译器版本、模块、替换项和构建设置 | 内置静态 ELF/PE/thin Mach-O 检查;无需安装 Go |
|
||||
| WASM 工件 | 验证、节、精确的导入/导出 WAT 形式及解码后的 WAT | 调用方提供的 WABT 1.0.42;`inspect_wasm_artifact` |
|
||||
| EVM 字节码 | 分派选择器、字节偏移、推断的参数和状态可变性 | Linux x64 上的本地原始字节/十六进制输入载体;[离线 EVM 指南](../evm-bytecode.md) |
|
||||
| Rust binaries | Embedded cargo-auditable dependency list | [Rust binaries](../rust-binaries.md) |
|
||||
| 已记录的 Linux 崩溃 | 原始 note 记录、每个已记录线程的寄存器/信号及可选的映射候选项 | Linux x64 上由调用方提供的 pwntools;可选的 GDB/pwndbg;[已记录崩溃](../recorded-crashes.md) |
|
||||
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](https://rea.tools/zh-CN/guides/javascript/) |
|
||||
| 网站 | 页面结构、脚本、网络观察结果和按请求获取的截图 | Chrome 系浏览器;[浏览器分析](https://rea.tools/zh-CN/guides/browser/) |
|
||||
| JavaScript / Electron | 模块、导入、source map、路由、IPC 和原生扩展关系 | Node.js 和 npm;[应用分析](../javascript-artifact-reconstruction.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) |
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Rust binary dependency metadata
|
||||
|
||||
`inspect_rust_binary` / `inspect-rust-binary` answers which crate names,
|
||||
versions, sources, and dependency edges are embedded by cargo-auditable in one
|
||||
selected local executable or library. It reads the file without running it and
|
||||
requires no Rust installation, active binary target, Hopper, Ghidra, or IDA.
|
||||
|
||||
```bash
|
||||
rea inspect-rust-binary /absolute/path/to/application --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_rust_binary",
|
||||
"arguments": { "path": "/artifacts/application" }
|
||||
}
|
||||
```
|
||||
|
||||
## Observations
|
||||
|
||||
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;
|
||||
- `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;
|
||||
- `packages`, in the section's original order, with `name`, `version`, `source`,
|
||||
`kind`, `dependencies`, and `root`;
|
||||
- `root_package_index`, the single package marked `root`, or `null` when none is marked;
|
||||
- `dependency_edges`, pairs of package indices in package-then-dependency order;
|
||||
- `cyclic_dependency_indices`, sorted unique indices that participate in a cycle.
|
||||
|
||||
An omitted `kind` stays `null`. The embedded format treats that omission as
|
||||
runtime, and that default is not stored in the section. An omitted dependency
|
||||
list is an empty array. An omitted `root` flag is false.
|
||||
|
||||
`rustc_commit` is present only when a `/rustc/<40-hex>/` path is found in
|
||||
read-only data. Its `evidence_kind` is `inferred`. The hash is not a claim that
|
||||
that compiler produced the binary. Several distinct hashes leave a single
|
||||
commit unknown.
|
||||
|
||||
## Limits and failures
|
||||
|
||||
The selected file is limited to 256 MiB. A dependency section is limited to 1
|
||||
MiB compressed. Inflated JSON is limited to the smaller of 8 MiB and 64 times
|
||||
the compressed size. The complete report is limited to 8 MiB. Empty sections,
|
||||
truncated or trailing zlib bytes, invalid JSON, unknown fields, dependency
|
||||
indices outside the package list, more than one root package, and compressed
|
||||
dependency sections are typed failures. A cycle is reported and is not walked.
|
||||
|
||||
A missing section returns success with `auditable_metadata: null` and a
|
||||
limitation. Absence of `.dep-v0` does not mean the binary has no dependencies.
|
||||
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.
|
||||
|
||||
Package and dependency counts are checked against the minimum required JSON
|
||||
representation before graph allocation or schema cloning. Oversized graphs
|
||||
report the output byte budget; the compiler-path capacity reports
|
||||
`maximum_rustc_hashes` in hashes, independently of byte limits.
|
||||
+18
-3
@@ -61,9 +61,24 @@ The tool profile preserves upstream release/source identity, resolved executable
|
||||
paths, executable digests, exact banners and arguments. Executable digests are
|
||||
checked again after each command. Caller-supplied tools are trusted programs;
|
||||
version and digest recording are not publisher authentication or an OS sandbox.
|
||||
Each subprocess has a 30-second deadline and 32 MiB combined output budget;
|
||||
truncation fails. All commands use existing owned-process cancellation and
|
||||
cleanup. Workspace cleanup failures remain explicit errors with resource paths.
|
||||
Each subprocess has a 30-second deadline and 32 MiB combined output budget.
|
||||
Inspection output past the budget fails with `resource_constraint`; an oversized
|
||||
`--version` probe still reports `unreadable_output`. Both streams share the
|
||||
budget, so a stream that starts after the other has filled it retains nothing.
|
||||
The captured output keeps up to the first 4,096 bytes that each stream retained,
|
||||
each stream's retained byte count, and the exit code and signal. Retained text
|
||||
can end in U+FFFD where the budget cut a multi-byte character.
|
||||
|
||||
These excerpts bound the returned diagnostic text.
|
||||
[PR #2117](https://github.com/morluto/rea/pull/2117) reports elapsed time, CPU
|
||||
time, and peak process-family memory, including WABT children, for warm Linux
|
||||
x64 WABT 1.0.42 CLI workflows; those costs did not change outside measurement
|
||||
noise. Agent token usage and truly cold workflows remain
|
||||
unmeasured. The underlying error cause still retains the full captured
|
||||
subprocess snapshot, so the excerpt does not establish lower retained memory
|
||||
or faster analysis.
|
||||
All commands use existing owned-process cancellation and cleanup. Workspace
|
||||
cleanup failures remain explicit errors with resource paths.
|
||||
|
||||
WAT is a decoded representation, not original source or proof of runtime
|
||||
behavior. Custom annotations can preserve payloads, but WAT does not establish original
|
||||
|
||||
@@ -28,6 +28,7 @@ export const toolFamilyCatalog = (sources) => {
|
||||
...sources.artifactContracts.ARTIFACT_TOOL_CONTRACTS,
|
||||
...sources.evmContracts.EVM_TOOL_CONTRACTS,
|
||||
...sources.goContracts.GO_TOOL_CONTRACTS,
|
||||
...sources.rustContracts.RUST_TOOL_CONTRACTS,
|
||||
...sources.wasmContracts.WASM_TOOL_CONTRACTS,
|
||||
...sources.sqliteContracts.SQLITE_TOOL_CONTRACTS,
|
||||
...sources.captureContracts.CAPTURE_TOOL_CONTRACTS,
|
||||
@@ -225,6 +226,10 @@ export const providerCatalog = (sources) => {
|
||||
identity: sources.goProvider.GO_BINARY_PROVIDER_IDENTITY,
|
||||
contracts: sources.goContracts.GO_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.rustProvider.RUST_BINARY_PROVIDER_IDENTITY,
|
||||
contracts: sources.rustContracts.RUST_TOOL_CONTRACTS,
|
||||
},
|
||||
{
|
||||
identity: sources.sqliteDomain.SQLITE_PROVIDER_IDENTITY,
|
||||
contracts: sources.sqliteContracts.SQLITE_TOOL_CONTRACTS,
|
||||
|
||||
@@ -59,6 +59,8 @@ const SOURCE_PATHS = {
|
||||
evmProvider: "dist/evm/EvmoleRelease.js",
|
||||
goContracts: "dist/contracts/go/goToolContracts.js",
|
||||
goProvider: "dist/go/GoBinaryProvider.js",
|
||||
rustContracts: "dist/contracts/rust/rustToolContracts.js",
|
||||
rustProvider: "dist/rust/RustBinaryProvider.js",
|
||||
artifactContracts: "dist/contracts/artifactToolContracts.js",
|
||||
managedContracts: "dist/contracts/managed/managedToolContracts.js",
|
||||
firmwareContracts: "dist/contracts/firmware/firmwareToolContracts.js",
|
||||
|
||||
@@ -37,7 +37,9 @@ export const assertCleanLinuxHopperRuntime = async (
|
||||
if (!entry.isDirectory() || !/^\d+$/u.test(entry.name)) continue;
|
||||
let executable;
|
||||
try {
|
||||
executable = await stat(`${procRoot}/${entry.name}/exe`, { bigint: true });
|
||||
executable = await stat(`${procRoot}/${entry.name}/exe`, {
|
||||
bigint: true,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error?.code === "ENOENT" || error?.code === "ESRCH") continue;
|
||||
// Other users' executables may be unreadable. The setup lane launches
|
||||
|
||||
@@ -8,6 +8,7 @@ const MIGRATED_PROVIDER_ROOTS = new Set([
|
||||
"sqlite",
|
||||
"evm",
|
||||
"go",
|
||||
"rust",
|
||||
"wasm",
|
||||
"captures",
|
||||
"signals",
|
||||
|
||||
@@ -8,4 +8,10 @@ export function assertLinuxPackageProviderFailure(execution: {
|
||||
export function verifyPackagePlatform(input: {
|
||||
readonly cli: string;
|
||||
readonly environment: Readonly<Record<string, string | undefined>>;
|
||||
readonly platform?: string;
|
||||
readonly runCommand?: (
|
||||
command: string,
|
||||
arguments_: readonly string[],
|
||||
environment: Readonly<Record<string, string | undefined>>,
|
||||
) => Promise<string>;
|
||||
}): Promise<void>;
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { json, run, runWithStatus } from "./lib/verify-package-core.mjs";
|
||||
|
||||
/** Run platform-specific deep-analysis smoke tests. */
|
||||
export async function verifyPackagePlatform({ cli, environment }) {
|
||||
if (process.platform === "linux") {
|
||||
export async function verifyPackagePlatform({
|
||||
cli,
|
||||
environment,
|
||||
platform = process.platform,
|
||||
runCommand = run,
|
||||
}) {
|
||||
if (platform === "linux") {
|
||||
const unsupportedExecution = await runWithStatus(
|
||||
cli,
|
||||
["analyze", process.execPath, "--json"],
|
||||
@@ -11,25 +16,27 @@ export async function verifyPackagePlatform({ cli, environment }) {
|
||||
assertLinuxPackageProviderFailure(unsupportedExecution);
|
||||
} else {
|
||||
const overview = json(
|
||||
await run(cli, ["analyze", process.execPath, "--json"], environment),
|
||||
await runCommand(
|
||||
cli,
|
||||
["analyze", process.execPath, "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
if (
|
||||
overview.operation !== "binary_overview" ||
|
||||
overview.normalized_result?.procedure_count < 1
|
||||
)
|
||||
if (!hasPositiveProcedureCount(overview))
|
||||
throw new Error(
|
||||
`packaged Hopper-backed analyze CLI failed: ${JSON.stringify(overview)}`,
|
||||
);
|
||||
const inspected = json(
|
||||
await run(cli, ["inspect", process.execPath, "--json"], environment),
|
||||
await runCommand(
|
||||
cli,
|
||||
["inspect", process.execPath, "--json"],
|
||||
environment,
|
||||
),
|
||||
);
|
||||
if (
|
||||
inspected.operation !== "binary_overview" ||
|
||||
inspected.normalized_result?.procedure_count < 1
|
||||
)
|
||||
if (!hasPositiveProcedureCount(inspected))
|
||||
throw new Error("packaged inspect CLI failed");
|
||||
const functionResult = json(
|
||||
await run(
|
||||
await runCommand(
|
||||
cli,
|
||||
["function", process.execPath, "0x1000", "--json"],
|
||||
environment,
|
||||
@@ -44,7 +51,7 @@ export async function verifyPackagePlatform({ cli, environment }) {
|
||||
`packaged function CLI failed: ${JSON.stringify(functionResult)}`,
|
||||
);
|
||||
const xrefs = json(
|
||||
await run(
|
||||
await runCommand(
|
||||
cli,
|
||||
["xrefs", process.execPath, "0x1000", "--json"],
|
||||
environment,
|
||||
@@ -56,7 +63,7 @@ export async function verifyPackagePlatform({ cli, environment }) {
|
||||
)
|
||||
throw new Error(`packaged xrefs CLI failed: ${JSON.stringify(xrefs)}`);
|
||||
const trace = json(
|
||||
await run(
|
||||
await runCommand(
|
||||
cli,
|
||||
["trace", process.execPath, "fixture", "--json"],
|
||||
environment,
|
||||
@@ -93,3 +100,10 @@ export const assertLinuxPackageProviderFailure = (execution) => {
|
||||
`packaged Linux Hopper verification did not fail closed: ${JSON.stringify(failure)}`,
|
||||
);
|
||||
};
|
||||
|
||||
const hasPositiveProcedureCount = (result) =>
|
||||
typeof result === "object" &&
|
||||
result !== null &&
|
||||
result.operation === "binary_overview" &&
|
||||
Number.isInteger(result.normalized_result?.procedure_count) &&
|
||||
result.normalized_result.procedure_count > 0;
|
||||
|
||||
@@ -40,10 +40,14 @@ import {
|
||||
} from "../domain/zipPackageFormat.js";
|
||||
|
||||
import {
|
||||
parseCodeViewData,
|
||||
parseExecutableHeader,
|
||||
parsePeRecord,
|
||||
peDebugDirectoryEntry,
|
||||
peRvaToFileOffset,
|
||||
type ExecutableMetadata,
|
||||
} from "../domain/binaryTarget.js";
|
||||
import type { PeCodeViewRecord } from "../domain/binaryTargetTypes.js";
|
||||
|
||||
/** Options for resolving a local binary target and its stable identity. */
|
||||
export interface BinaryTargetResolverOptions {
|
||||
@@ -549,10 +553,98 @@ const readPeMetadata = async (
|
||||
return err(`unsupported ${windowsHeader.signature} executable in MZ image`);
|
||||
const optionalHeaderSize = fileHeader.readUInt16LE(20);
|
||||
if (optionalHeaderSize > 4096) return err("invalid PE optional header size");
|
||||
const numberOfSections = fileHeader.readUInt16LE(6);
|
||||
const record = Buffer.alloc(24 + optionalHeaderSize);
|
||||
const recordRead = await handle.read(record, 0, record.length, offset);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
return recordRead.bytesRead === record.length
|
||||
? parsePeRecord(record)
|
||||
: err("invalid or truncated PE header");
|
||||
if (recordRead.bytesRead !== record.length)
|
||||
return err("invalid or truncated PE header");
|
||||
const codeView = await readPeCodeView(
|
||||
handle,
|
||||
offset,
|
||||
numberOfSections,
|
||||
record.subarray(24),
|
||||
optionalHeaderSize,
|
||||
signal,
|
||||
);
|
||||
return parsePeRecord(record, codeView);
|
||||
};
|
||||
|
||||
const readPeCodeView = async (
|
||||
handle: FileHandle,
|
||||
peOffset: number,
|
||||
numberOfSections: number,
|
||||
optionalHeader: Buffer,
|
||||
optionalHeaderSize: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<PeCodeViewRecord | undefined> => {
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const debugDir = peDebugDirectoryEntry(optionalHeader);
|
||||
if (debugDir === undefined || numberOfSections > 96) return undefined;
|
||||
const sectionTableOffset = peOffset + 24 + optionalHeaderSize;
|
||||
const sectionTableSize = numberOfSections * 40;
|
||||
const sectionTable = Buffer.alloc(sectionTableSize);
|
||||
const sectionRead = await handle.read(
|
||||
sectionTable,
|
||||
0,
|
||||
sectionTableSize,
|
||||
sectionTableOffset,
|
||||
);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
if (sectionRead.bytesRead !== sectionTableSize) return undefined;
|
||||
const debugFileOffset = peRvaToFileOffset(
|
||||
debugDir.debugRva,
|
||||
sectionTable,
|
||||
numberOfSections,
|
||||
debugDir.sizeOfHeaders,
|
||||
Math.min(debugDir.debugSize, 4096),
|
||||
);
|
||||
if (debugFileOffset === undefined) return undefined;
|
||||
const maxDebugSize = Math.min(debugDir.debugSize, 4096);
|
||||
const debugDirectoryBuffer = Buffer.alloc(maxDebugSize);
|
||||
const debugDirRead = await handle.read(
|
||||
debugDirectoryBuffer,
|
||||
0,
|
||||
maxDebugSize,
|
||||
debugFileOffset,
|
||||
);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const numEntries = Math.floor(debugDirRead.bytesRead / 28);
|
||||
for (let j = 0; j < numEntries; j++) {
|
||||
const entryOffset = j * 28;
|
||||
const type = debugDirectoryBuffer.readUInt32LE(entryOffset + 12);
|
||||
if (type !== 2) continue; // IMAGE_DEBUG_TYPE_CODEVIEW
|
||||
const sizeOfData = debugDirectoryBuffer.readUInt32LE(entryOffset + 16);
|
||||
let pointerToRawData = debugDirectoryBuffer.readUInt32LE(entryOffset + 24);
|
||||
if (pointerToRawData === 0) {
|
||||
const addressOfRawData = debugDirectoryBuffer.readUInt32LE(
|
||||
entryOffset + 20,
|
||||
);
|
||||
const mapped = peRvaToFileOffset(
|
||||
addressOfRawData,
|
||||
sectionTable,
|
||||
numberOfSections,
|
||||
debugDir.sizeOfHeaders,
|
||||
Math.min(sizeOfData, 4096),
|
||||
);
|
||||
if (mapped === undefined) continue;
|
||||
pointerToRawData = mapped;
|
||||
}
|
||||
if (sizeOfData < 16) continue;
|
||||
const maxCodeViewRead = Math.min(sizeOfData, 4096);
|
||||
const codeViewBuffer = Buffer.alloc(maxCodeViewRead);
|
||||
const codeViewRead = await handle.read(
|
||||
codeViewBuffer,
|
||||
0,
|
||||
maxCodeViewRead,
|
||||
pointerToRawData,
|
||||
);
|
||||
throwIfTargetResolutionCancelled(signal);
|
||||
const parsed = parseCodeViewData(
|
||||
codeViewBuffer.subarray(0, codeViewRead.bytesRead),
|
||||
sizeOfData,
|
||||
);
|
||||
if (parsed !== undefined) return parsed;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
@@ -343,7 +343,7 @@ const workflowAvailabilityFor = ({
|
||||
kind,
|
||||
policy,
|
||||
}: AvailabilityContext): Availability | null => {
|
||||
if (name === "inspect_go_binary")
|
||||
if (name === "inspect_go_binary" || name === "inspect_rust_binary")
|
||||
return { reason: "available", remediation: null };
|
||||
if (name === "inspect_sqlite_database") {
|
||||
const capability = sqliteDatabaseCapability(process.versions.node);
|
||||
|
||||
@@ -186,6 +186,28 @@ describe("analysis provider registry: binding failures and cancellation", () =>
|
||||
expect(explicit.error.reason).toBe("target_unsupported");
|
||||
});
|
||||
|
||||
it("rejects an unknown environment preference even when every candidate rejects the target", async () => {
|
||||
const alpha = candidate("alpha");
|
||||
const misconfigured = new AnalysisProviderRegistry(
|
||||
[alpha.provider],
|
||||
"missing",
|
||||
);
|
||||
|
||||
for (const selected of [
|
||||
await misconfigured.select(ARTIFACT_TARGET),
|
||||
await misconfigured.select(ARTIFACT_TARGET, "missing"),
|
||||
]) {
|
||||
if (selected.ok) throw new Error("expected unknown provider rejection");
|
||||
expect(projectAnalysisError(selected.error)).toMatchObject({
|
||||
details: {
|
||||
selection_reason: "unknown_provider",
|
||||
requested_provider_id: "missing",
|
||||
candidate_ids: ["alpha"],
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("does not bind a profile with unresolved version or adapter failure", async () => {
|
||||
const unresolved = candidate("unresolved", { profile: "missing" });
|
||||
const failed = candidate("failed", { profile: "error" });
|
||||
|
||||
@@ -176,7 +176,7 @@ export class AnalysisProviderRegistry {
|
||||
return err(new AnalysisCancelledError("open_binary"));
|
||||
if (
|
||||
request === undefined &&
|
||||
selector !== "auto" &&
|
||||
this.#byId.has(selector) &&
|
||||
baseline.length > 0 &&
|
||||
baseline.every(
|
||||
({ targetSupport }) => targetSupport.status === "unsupported",
|
||||
|
||||
@@ -1023,6 +1023,28 @@ it("serializes script loading against concurrent close and unloads the loaded sc
|
||||
expect(bindings.device.session.isDetached()).toBe(true);
|
||||
});
|
||||
|
||||
it("drains startup when the provider loader initiates shutdown", async () => {
|
||||
const bindings = new FakeDeviceManager();
|
||||
let closing: Promise<void> | undefined;
|
||||
const manager = new FridaInstrumentationManager({
|
||||
deviceManagerLoader: async () => {
|
||||
closing = manager.closeAll();
|
||||
return bindings;
|
||||
},
|
||||
});
|
||||
const started = await manager.startSession({
|
||||
mode: "spawn",
|
||||
deviceId: "local",
|
||||
program: "/tmp/fixture",
|
||||
});
|
||||
if (!started.ok) throw started.error;
|
||||
expect(closing).toBeDefined();
|
||||
await closing;
|
||||
expect(bindings.device.session.isDetached()).toBe(true);
|
||||
expect(bindings.device.resumed).toEqual([512]);
|
||||
expect(manager.status(started.value.sessionId)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("names the unknown session or script ID it rejects", async () => {
|
||||
const { manager, started } = await startLocalAttachedSession();
|
||||
if (!started.ok) throw started.error;
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type {
|
||||
AnalysisError,
|
||||
AnalysisErrorOptions,
|
||||
} from "../../domain/analysisErrorBase.js";
|
||||
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
|
||||
import type { OptionalProviderLoadFailure } from "../OptionalObservationProviders.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import type { Result } from "../../domain/result.js";
|
||||
@@ -60,6 +64,15 @@ export interface FridaScriptObservation {
|
||||
readonly messagesTruncated: boolean;
|
||||
}
|
||||
|
||||
export class FridaScriptLoadCancelledError extends AnalysisCancelledError {
|
||||
constructor(
|
||||
readonly script: FridaScriptObservation,
|
||||
options?: AnalysisErrorOptions,
|
||||
) {
|
||||
super("load_frida_script", options);
|
||||
}
|
||||
}
|
||||
|
||||
export interface FridaSessionStatus extends FridaSessionObservation {
|
||||
/** True only after REA has successfully resumed this spawned target. */
|
||||
readonly targetResumeObserved: boolean;
|
||||
@@ -100,14 +113,20 @@ export interface FridaInstrumentationPort {
|
||||
startSession(
|
||||
input: StartFridaSessionInput,
|
||||
): Promise<Result<FridaSessionObservation, AnalysisError>>;
|
||||
/** Own cancellation and new-script cleanup before returning a final load outcome. */
|
||||
loadScript(
|
||||
sessionId: string,
|
||||
source: FridaScriptSource,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<FridaScriptObservation, AnalysisError>>;
|
||||
resumeSession(sessionId: string): Promise<Result<null, AnalysisError>>;
|
||||
resumeSession(
|
||||
sessionId: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<null, AnalysisError>>;
|
||||
unloadScript(
|
||||
sessionId: string,
|
||||
scriptId: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<null, AnalysisError>>;
|
||||
status(sessionId: string): FridaSessionStatus | undefined;
|
||||
closeSession(
|
||||
|
||||
@@ -3,6 +3,7 @@ import { join } from "node:path";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { FridaScriptLoadCancelledError } from "./FridaInstrumentationPort.js";
|
||||
import type { FridaInstrumentationPort } from "./FridaInstrumentationPort.js";
|
||||
import { FridaInstrumentationService } from "./FridaInstrumentationService.js";
|
||||
import {
|
||||
@@ -569,3 +570,74 @@ it("cleans up a session and returns typed cancellation after a pending start", a
|
||||
if (!result.ok) expect(result.error._tag).toBe("AnalysisCancelledError");
|
||||
expect(closed).toEqual(["00000000-0000-4000-8000-000000000001"]);
|
||||
});
|
||||
|
||||
it("retains a cancelled provider load's source and messages in one-shot Evidence", async () => {
|
||||
const controller = new AbortController();
|
||||
const script = {
|
||||
...createScriptObservation(),
|
||||
messages: [{ type: "send", payload: "cancelled-load" }],
|
||||
};
|
||||
const service = new FridaInstrumentationService(
|
||||
createProvider({
|
||||
async loadScript() {
|
||||
controller.abort(new Error("cancel during native load"));
|
||||
return {
|
||||
ok: false,
|
||||
error: new FridaScriptLoadCancelledError(script, {
|
||||
cause: controller.signal.reason,
|
||||
}),
|
||||
};
|
||||
},
|
||||
}),
|
||||
);
|
||||
const result = await service.instrument(
|
||||
{
|
||||
mode: "spawn",
|
||||
program: "/fixture",
|
||||
source: { sourceKind: "inline", source: "send('cancelled-load')" },
|
||||
durationMs: 0,
|
||||
},
|
||||
controller.signal,
|
||||
);
|
||||
if (result.ok) throw new Error("Cancelled instrumentation succeeded");
|
||||
expect(result.error.partialObservation).toMatchObject({
|
||||
normalized_result: {
|
||||
source_kind: "inline",
|
||||
source_sha256: script.sourceSha256,
|
||||
messages: script.messages,
|
||||
instrumentation_cleanup: "released",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves a provider-committed load when cancellation arrives during result delivery", async () => {
|
||||
const controller = new AbortController();
|
||||
let unloads = 0;
|
||||
const script = createScriptObservation();
|
||||
const service = new FridaInstrumentationService(
|
||||
createProvider({
|
||||
loadScript() {
|
||||
const completed = Promise.resolve({ ok: true as const, value: script });
|
||||
void completed.then(() =>
|
||||
controller.abort(new Error("late cancellation")),
|
||||
);
|
||||
return completed;
|
||||
},
|
||||
async unloadScript() {
|
||||
unloads += 1;
|
||||
return { ok: true, value: null };
|
||||
},
|
||||
}),
|
||||
);
|
||||
const loaded = await service.loadScript(
|
||||
createSession().sessionId,
|
||||
{ sourceKind: "inline", source: "send('ready')" },
|
||||
controller.signal,
|
||||
);
|
||||
expect(controller.signal.aborted).toBe(true);
|
||||
if (!loaded.ok) throw loaded.error;
|
||||
expect(loaded.value.normalized_result).toMatchObject({
|
||||
script_id: script.scriptId,
|
||||
});
|
||||
expect(unloads).toBe(0);
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
AnalysisInputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { FridaScriptLoadCancelledError } from "./FridaInstrumentationPort.js";
|
||||
import type {
|
||||
FridaDeviceSelector,
|
||||
FridaInstrumentationPort,
|
||||
@@ -181,8 +182,17 @@ export class FridaInstrumentationService {
|
||||
async loadScript(
|
||||
sessionId: string,
|
||||
source: FridaScriptSource,
|
||||
signal?: AbortSignal,
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
const loaded = await this.#provider.loadScript(sessionId, source);
|
||||
if (signal?.aborted)
|
||||
return err(
|
||||
new AnalysisCancelledError("load_frida_script", {
|
||||
cause: signal.reason,
|
||||
}),
|
||||
);
|
||||
const loaded = await this.#provider.loadScript(sessionId, source, signal);
|
||||
if (!loaded.ok && loaded.error instanceof FridaScriptLoadCancelledError)
|
||||
return err(cancelledScriptEvidence(sessionId, loaded.error));
|
||||
return loaded.ok
|
||||
? ok(
|
||||
fridaEvidence("load_frida_script", scriptOutput(loaded.value), {
|
||||
@@ -192,12 +202,12 @@ export class FridaInstrumentationService {
|
||||
: loaded;
|
||||
}
|
||||
|
||||
resumeSession(sessionId: string) {
|
||||
return this.#provider.resumeSession(sessionId);
|
||||
resumeSession(sessionId: string, signal?: AbortSignal) {
|
||||
return this.#provider.resumeSession(sessionId, signal);
|
||||
}
|
||||
|
||||
unloadScript(sessionId: string, scriptId: string) {
|
||||
return this.#provider.unloadScript(sessionId, scriptId);
|
||||
unloadScript(sessionId: string, scriptId: string, signal?: AbortSignal) {
|
||||
return this.#provider.unloadScript(sessionId, scriptId, signal);
|
||||
}
|
||||
|
||||
status(sessionId: string): FridaSessionStatus | undefined {
|
||||
@@ -243,14 +253,19 @@ export class FridaInstrumentationService {
|
||||
const loaded = await this.#provider.loadScript(
|
||||
started.value.sessionId,
|
||||
input.source,
|
||||
signal,
|
||||
);
|
||||
let scriptObservation: FridaScriptObservation | undefined;
|
||||
if (loaded.ok) scriptObservation = loaded.value;
|
||||
else if (loaded.error instanceof FridaScriptLoadCancelledError)
|
||||
scriptObservation = loaded.error.script;
|
||||
if (signal?.aborted)
|
||||
return this.#cancelSession(
|
||||
"instrument_with_frida",
|
||||
started.value,
|
||||
input,
|
||||
signal.reason,
|
||||
loaded.ok ? loaded.value : undefined,
|
||||
scriptObservation,
|
||||
);
|
||||
if (!loaded.ok) {
|
||||
const closed = await this.#provider.closeSession(started.value.sessionId);
|
||||
@@ -267,6 +282,7 @@ export class FridaInstrumentationService {
|
||||
if (started.value.mode === "spawn") {
|
||||
const resumed = await this.#provider.resumeSession(
|
||||
started.value.sessionId,
|
||||
signal,
|
||||
);
|
||||
if (signal?.aborted)
|
||||
return this.#cancelSession(
|
||||
@@ -462,3 +478,23 @@ const sessionStatusOutput = (status: FridaSessionStatus) => ({
|
||||
messages: status.messages,
|
||||
messages_truncated: status.messagesTruncated,
|
||||
});
|
||||
|
||||
const cancelledScriptEvidence = (
|
||||
sessionId: string,
|
||||
error: FridaScriptLoadCancelledError,
|
||||
): AnalysisCancelledError =>
|
||||
new AnalysisCancelledError(error.operation, {
|
||||
cause: error.cause,
|
||||
cleanup: error.cleanup,
|
||||
partialObservation: fridaEvidence(
|
||||
error.operation,
|
||||
{
|
||||
...scriptOutput(error.script),
|
||||
cleanup_error: error.cleanup?.reason ?? null,
|
||||
script_cleanup: error.cleanupIncomplete
|
||||
? "incomplete_or_unknown"
|
||||
: "released",
|
||||
},
|
||||
{ session_id: sessionId },
|
||||
),
|
||||
});
|
||||
|
||||
@@ -92,7 +92,11 @@ export const compareManagedMemberPaths = async (
|
||||
throw new EvidenceIntegrityError(
|
||||
`Managed artifact digest changed after open: expected ${target.sha256}, observed ${snapshot.sha256} at ${target.path}`,
|
||||
);
|
||||
const inspection = inspectManagedMembersBytes(snapshot.bytes, target);
|
||||
// Member comparison never reads literals, so it does not spend the
|
||||
// decode budget on them.
|
||||
const inspection = inspectManagedMembersBytes(snapshot.bytes, target, {
|
||||
userStrings: false,
|
||||
});
|
||||
const evidence = createEvidence(target, MANAGED_STATIC_PROVIDER, {
|
||||
operation: "inspect_managed_members",
|
||||
parameters: {},
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import type {
|
||||
ExecutionOptions,
|
||||
ProviderIdentity,
|
||||
} from "../AnalysisProvider.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type {
|
||||
InspectRustBinaryInput,
|
||||
RustBinary,
|
||||
} from "../../domain/rust/rustBinary.js";
|
||||
import type { Result } from "../../domain/result.js";
|
||||
|
||||
/** Replaceable offline dependency reader with no target-execution authority. */
|
||||
export interface RustBinaryPort {
|
||||
readonly identity: ProviderIdentity;
|
||||
inspect(
|
||||
input: InspectRustBinaryInput,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<RustBinary, AnalysisError>>;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import type { RustBinary } from "../../domain/rust/rustBinary.js";
|
||||
import { ok } from "../../domain/result.js";
|
||||
import { RustBinaryService } from "./RustBinaryService.js";
|
||||
|
||||
const identity = {
|
||||
id: "fixture-rust",
|
||||
name: "Source-owned Rust reader seam",
|
||||
version: "1",
|
||||
};
|
||||
const report: RustBinary = {
|
||||
artifact: { path: "/artifacts/program", sha256: "a".repeat(64), bytes: 128 },
|
||||
format: "elf",
|
||||
architecture: "amd64",
|
||||
bits: 64,
|
||||
byte_order: "little",
|
||||
auditable_metadata: {
|
||||
section_name: ".dep-v0",
|
||||
file_offset: 16,
|
||||
section_bytes: 8,
|
||||
compressed_bytes: 8,
|
||||
uncompressed_bytes: 20,
|
||||
format_version: 1,
|
||||
packages: [
|
||||
{
|
||||
name: "sample",
|
||||
version: "0.1.0",
|
||||
source: "local",
|
||||
kind: null,
|
||||
dependencies: [],
|
||||
root: true,
|
||||
},
|
||||
],
|
||||
root_package_index: 0,
|
||||
dependency_edges: [],
|
||||
cyclic_dependency_indices: [],
|
||||
},
|
||||
rustc_commit: null,
|
||||
limitations: [
|
||||
"No /rustc/<40-hex>/ path was found in read-only data, so a rustc commit hash is unknown.",
|
||||
],
|
||||
};
|
||||
|
||||
it("rejects a relative path and extra arguments before reading", async () => {
|
||||
let calls = 0;
|
||||
const service = new RustBinaryService({
|
||||
identity,
|
||||
inspect: () => {
|
||||
calls += 1;
|
||||
return Promise.resolve(ok(report));
|
||||
},
|
||||
});
|
||||
const relative = await service.inspect({ path: "program" });
|
||||
const extra = await service.inspect({
|
||||
path: report.artifact.path,
|
||||
detail: "complete",
|
||||
});
|
||||
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 artifact and the dependency section range", async () => {
|
||||
const result = await new RustBinaryService({
|
||||
identity,
|
||||
inspect: () => Promise.resolve(ok(report)),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value).toMatchObject({
|
||||
confidence: "observed",
|
||||
subject: { format: "elf", architecture: "x86_64" },
|
||||
normalized_result: report,
|
||||
locations: [
|
||||
{ kind: "artifact-path", path: report.artifact.path },
|
||||
{ kind: "file-offset-range", start: 16, end: 24 },
|
||||
],
|
||||
});
|
||||
const mismatched: RustBinary = {
|
||||
...report,
|
||||
artifact: { ...report.artifact, path: "/artifacts/other" },
|
||||
};
|
||||
const invalid = await new RustBinaryService({
|
||||
identity,
|
||||
inspect: () => Promise.resolve(ok(mismatched)),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
expect(invalid.ok).toBe(false);
|
||||
if (!invalid.ok) expect(invalid.error).toBeInstanceOf(AnalysisOutputError);
|
||||
});
|
||||
|
||||
it("rejects a cycle claim the package dependencies do not contain", async () => {
|
||||
if (report.auditable_metadata === null) throw new Error("expected metadata");
|
||||
const invalidReport: RustBinary = {
|
||||
...report,
|
||||
auditable_metadata: {
|
||||
...report.auditable_metadata,
|
||||
cyclic_dependency_indices: [0],
|
||||
},
|
||||
};
|
||||
const result = await new RustBinaryService({
|
||||
identity,
|
||||
inspect: () => Promise.resolve(ok(invalidReport)),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ from_index: 1, to_index: 0 },
|
||||
{ from_index: 0, to_index: 1 },
|
||||
])("rejects provider edges referencing missing packages: %j", async (edge) => {
|
||||
if (report.auditable_metadata === null) throw new Error("expected metadata");
|
||||
const invalidReport: RustBinary = {
|
||||
...report,
|
||||
auditable_metadata: {
|
||||
...report.auditable_metadata,
|
||||
dependency_edges: [edge],
|
||||
},
|
||||
};
|
||||
const result = await new RustBinaryService({
|
||||
identity,
|
||||
inspect: () => Promise.resolve(ok(invalidReport)),
|
||||
}).inspect({ path: report.artifact.path });
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) expect(result.error).toBeInstanceOf(AnalysisOutputError);
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
import { isAbsolute } from "node:path";
|
||||
import type { ExecutionOptions } from "../AnalysisProvider.js";
|
||||
import type { RustBinaryPort } from "./RustBinaryPort.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 { BINARY_ARCHITECTURES } from "../../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
inspectRustBinaryInputSchema,
|
||||
rustBinarySchema,
|
||||
} from "../../domain/rust/rustBinary.js";
|
||||
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
|
||||
const OPERATION = "inspect_rust_binary";
|
||||
|
||||
const architectureForSubject = (architecture: string) => {
|
||||
switch (architecture) {
|
||||
case "386":
|
||||
return "x86";
|
||||
case "amd64":
|
||||
return "x86_64";
|
||||
default:
|
||||
return BINARY_ARCHITECTURES.find(
|
||||
(supported) => supported === architecture,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/** Shared exact input validation and artifact-bound Evidence for CLI and MCP. */
|
||||
export class RustBinaryService {
|
||||
constructor(readonly provider: RustBinaryPort) {}
|
||||
|
||||
/** Inspect one explicitly selected file without acquiring a binary session. */
|
||||
async inspect(
|
||||
rawInput: unknown,
|
||||
options?: ExecutionOptions,
|
||||
): Promise<Result<Evidence, AnalysisError>> {
|
||||
if (options?.signal?.aborted)
|
||||
return err(new AnalysisCancelledError(OPERATION));
|
||||
const input = inspectRustBinaryInputSchema.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 inspected = await this.provider.inspect(input.data, options);
|
||||
if (!inspected.ok) return inspected;
|
||||
if (options?.signal?.aborted)
|
||||
return err(new AnalysisCancelledError(OPERATION));
|
||||
const report = rustBinarySchema.safeParse(inspected.value);
|
||||
if (!report.success || report.data.artifact.path !== input.data.path)
|
||||
return err(
|
||||
new AnalysisOutputError(
|
||||
OPERATION,
|
||||
"Rust dependency reader returned malformed metadata or changed the selected artifact identity.",
|
||||
),
|
||||
);
|
||||
return ok(evidenceFor(this.provider.identity, input.data, report.data));
|
||||
}
|
||||
}
|
||||
|
||||
const evidenceFor = (
|
||||
provider: RustBinaryPort["identity"],
|
||||
parameters: { readonly path: string },
|
||||
value: ReturnType<typeof rustBinarySchema.parse>,
|
||||
): Evidence => {
|
||||
const architecture = architectureForSubject(value.architecture);
|
||||
const section = value.auditable_metadata;
|
||||
return createEvidence(
|
||||
{
|
||||
path: value.artifact.path,
|
||||
sha256: value.artifact.sha256,
|
||||
format: value.format === "macho" ? "mach-o" : value.format,
|
||||
...(architecture === undefined ? {} : { architecture }),
|
||||
},
|
||||
provider,
|
||||
{
|
||||
operation: OPERATION,
|
||||
parameters,
|
||||
result: value,
|
||||
confidence: "observed",
|
||||
limitations: value.limitations,
|
||||
locations: [
|
||||
{ kind: "artifact-path", path: value.artifact.path },
|
||||
...(section === null
|
||||
? []
|
||||
: [
|
||||
{
|
||||
kind: "file-offset-range" as const,
|
||||
start: section.file_offset,
|
||||
end: section.file_offset + section.section_bytes,
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
);
|
||||
};
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
type AnalysisInputIssue,
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import {
|
||||
@@ -41,22 +42,29 @@ export class WasmArtifactService {
|
||||
cause: input.error,
|
||||
}),
|
||||
);
|
||||
if (
|
||||
![
|
||||
input.data.path,
|
||||
...input.data.glue_paths,
|
||||
...input.data.candidate_paths,
|
||||
].every(isAbsolute)
|
||||
)
|
||||
return err(
|
||||
new AnalysisInputError(OPERATION, undefined, [
|
||||
{
|
||||
path: ["path"],
|
||||
reason: "invalid_format",
|
||||
message: "Expected an absolute filesystem path on this host.",
|
||||
},
|
||||
]),
|
||||
);
|
||||
const pathIssues = [
|
||||
{ value: input.data.path, path: ["path"] },
|
||||
...input.data.glue_paths.map((value, index) => ({
|
||||
value,
|
||||
path: ["glue_paths", index],
|
||||
})),
|
||||
...input.data.candidate_paths.map((value, index) => ({
|
||||
value,
|
||||
path: ["candidate_paths", index],
|
||||
})),
|
||||
].flatMap<AnalysisInputIssue>(({ value, path }) =>
|
||||
isAbsolute(value)
|
||||
? []
|
||||
: [
|
||||
{
|
||||
path,
|
||||
reason: "invalid_format",
|
||||
message: "Expected an absolute filesystem path on this host.",
|
||||
},
|
||||
],
|
||||
);
|
||||
if (pathIssues.length > 0)
|
||||
return err(new AnalysisInputError(OPERATION, undefined, pathIssues));
|
||||
const inspected = await this.provider.inspect(input.data, options);
|
||||
if (!inspected.ok) return inspected;
|
||||
if (options?.signal?.aborted)
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Readable } from "node:stream";
|
||||
import type { ArtifactCommand } from "../domain/artifactGraph.js";
|
||||
import type {
|
||||
ArtifactCommand,
|
||||
ArtifactInventoryCoverage,
|
||||
} from "../domain/artifactGraph.js";
|
||||
import type { ZipPackageFormat } from "../domain/zipPackageFormat.js";
|
||||
import type {
|
||||
AnalysisCleanupObservation,
|
||||
@@ -90,10 +93,36 @@ export const sameArtifactEntry = (
|
||||
entry.sourceIdentity?.mtimeNs === produced.sourceIdentity?.mtimeNs &&
|
||||
entry.sourceIdentity?.ctimeNs === produced.sourceIdentity?.ctimeNs;
|
||||
|
||||
/** One entry or subtree deliberately excluded from a traversal. */
|
||||
export interface ArtifactTraversalOmission {
|
||||
readonly path: string;
|
||||
readonly kind:
|
||||
| "fifo"
|
||||
| "socket"
|
||||
| "block-device"
|
||||
| "character-device"
|
||||
| "unsupported"
|
||||
| "directory-descendants";
|
||||
}
|
||||
|
||||
/** Facts from an exhausted traversal, separate from entry content certainty. */
|
||||
export interface ArtifactReaderTraversal {
|
||||
readonly status: ArtifactInventoryCoverage["status"];
|
||||
readonly limitations: readonly string[];
|
||||
}
|
||||
|
||||
/** Per-iterator observation sink; adapters report intentional omissions without shared state. */
|
||||
export interface ArtifactTraversalOptions {
|
||||
readonly onOmission?: (omission: ArtifactTraversalOmission) => void;
|
||||
}
|
||||
|
||||
/** Read-only adapter over one directory, archive, or virtual container. */
|
||||
export interface ArtifactReader {
|
||||
readonly format: "directory" | ZipPackageFormat | "asar" | "file";
|
||||
entries(signal?: AbortSignal): AsyncIterable<ArtifactEntry>;
|
||||
entries(
|
||||
signal?: AbortSignal,
|
||||
options?: ArtifactTraversalOptions,
|
||||
): AsyncIterable<ArtifactEntry>;
|
||||
open(entry: ArtifactEntry, signal?: AbortSignal): Promise<Readable>;
|
||||
provenance(): readonly ArtifactCommand[];
|
||||
close(): Promise<void>;
|
||||
|
||||
@@ -14,12 +14,18 @@ import {
|
||||
sameArtifactSourceIdentity,
|
||||
type ArtifactEntry,
|
||||
type ArtifactReader,
|
||||
type ArtifactTraversalOmission,
|
||||
type ArtifactTraversalOptions,
|
||||
} from "./ArtifactReader.js";
|
||||
import { hostExecutableMarking } from "./hostExecutableMarking.js";
|
||||
import { OwnedDirectoryHandle } from "../filesystem/OwnedDirectoryHandle.js";
|
||||
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
|
||||
import { readFileHandleChunks } from "../filesystem/readFileHandleChunks.js";
|
||||
|
||||
interface DirectoryTraversalOptions extends ArtifactTraversalOptions {
|
||||
readonly descendInto?: (path: string) => boolean;
|
||||
}
|
||||
|
||||
/** Non-symlink-following directory reader. */
|
||||
export class DirectoryArtifactReader implements ArtifactReader {
|
||||
readonly format = "directory" as const;
|
||||
@@ -35,9 +41,11 @@ export class DirectoryArtifactReader implements ArtifactReader {
|
||||
/** Enumerate entries, optionally restricting descent while retaining directories. */
|
||||
async *entries(
|
||||
signal?: AbortSignal,
|
||||
descendInto?: (path: string) => boolean,
|
||||
selection?: DirectoryTraversalOptions | ((path: string) => boolean),
|
||||
): AsyncIterable<ArtifactEntry> {
|
||||
const root = await this.#resolveRoot();
|
||||
const options =
|
||||
typeof selection === "function" ? { descendInto: selection } : selection;
|
||||
const pending = [root];
|
||||
while (pending.length > 0) {
|
||||
abortIfNeeded(signal);
|
||||
@@ -50,7 +58,7 @@ export class DirectoryArtifactReader implements ArtifactReader {
|
||||
directory,
|
||||
pending,
|
||||
signal,
|
||||
descendInto,
|
||||
options,
|
||||
))
|
||||
yield entry;
|
||||
await withDirectoryIoContext("inspect directory", directory, () =>
|
||||
@@ -75,7 +83,7 @@ export class DirectoryArtifactReader implements ArtifactReader {
|
||||
directory: string,
|
||||
pending: string[],
|
||||
signal?: AbortSignal,
|
||||
descendInto?: (path: string) => boolean,
|
||||
options?: DirectoryTraversalOptions,
|
||||
): AsyncIterable<ArtifactEntry> {
|
||||
const root = await this.#resolveRoot();
|
||||
const handle = new OwnedDirectoryHandle(
|
||||
@@ -103,10 +111,16 @@ export class DirectoryArtifactReader implements ArtifactReader {
|
||||
);
|
||||
const path = relative(root, absolute).split(sep).join("/");
|
||||
const entry = artifactEntry(path, absolute, metadata);
|
||||
if (entry === undefined) continue;
|
||||
if (entry === undefined) {
|
||||
options?.onOmission?.({ path, kind: unsupportedEntryKind(metadata) });
|
||||
continue;
|
||||
}
|
||||
yield entry;
|
||||
if (entry.kind === "directory" && (descendInto?.(path) ?? true))
|
||||
childDirectories.push(absolute);
|
||||
if (entry.kind === "directory") {
|
||||
if (options?.descendInto?.(path) ?? true)
|
||||
childDirectories.push(absolute);
|
||||
else options?.onOmission?.({ path, kind: "directory-descendants" });
|
||||
}
|
||||
}
|
||||
} catch (cause: unknown) {
|
||||
traversal = {
|
||||
@@ -256,6 +270,19 @@ export class DirectoryArtifactReader implements ArtifactReader {
|
||||
}
|
||||
}
|
||||
|
||||
const unsupportedEntryKind = (
|
||||
metadata: BigIntStats,
|
||||
): ArtifactTraversalOmission["kind"] =>
|
||||
metadata.isFIFO()
|
||||
? "fifo"
|
||||
: metadata.isSocket()
|
||||
? "socket"
|
||||
: metadata.isBlockDevice()
|
||||
? "block-device"
|
||||
: metadata.isCharacterDevice()
|
||||
? "character-device"
|
||||
: "unsupported";
|
||||
|
||||
const artifactEntry = (
|
||||
path: string,
|
||||
absolute: string,
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
ArtifactReaderFailure,
|
||||
type ArtifactEntry,
|
||||
type ArtifactReader,
|
||||
type ArtifactTraversalOptions,
|
||||
} from "./ArtifactReader.js";
|
||||
import { DirectoryArtifactReader } from "./DirectoryArtifactReader.js";
|
||||
import {
|
||||
@@ -149,13 +150,24 @@ export class NativeDmgArtifactReader implements ArtifactReader {
|
||||
this.host = host ?? createSystemHost(environment);
|
||||
}
|
||||
|
||||
async *entries(signal?: AbortSignal): AsyncIterable<ArtifactEntry> {
|
||||
async *entries(
|
||||
signal?: AbortSignal,
|
||||
options?: ArtifactTraversalOptions,
|
||||
): AsyncIterable<ArtifactEntry> {
|
||||
await this.#ensureAttached(signal);
|
||||
const directory = this.#directory;
|
||||
if (directory === undefined)
|
||||
throw new ArtifactReaderFailure("unavailable", "DMG is not attached");
|
||||
const prefix = basename(this.path);
|
||||
const entries = directory.entries(signal)[Symbol.asyncIterator]();
|
||||
const entries = directory
|
||||
.entries(signal, {
|
||||
onOmission: (omission) =>
|
||||
options?.onOmission?.({
|
||||
...omission,
|
||||
path: `${prefix}/${omission.path}`,
|
||||
}),
|
||||
})
|
||||
[Symbol.asyncIterator]();
|
||||
try {
|
||||
for (;;) {
|
||||
if (this.#closed) return;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { AsarArtifactReader } from "../AsarArtifactReader.js";
|
||||
import type { ArtifactReader } from "../ArtifactReader.js";
|
||||
import type {
|
||||
ArtifactReader,
|
||||
ArtifactReaderTraversal,
|
||||
} from "../ArtifactReader.js";
|
||||
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
|
||||
import { MachOSliceArtifactReader } from "../MachOSliceArtifactReader.js";
|
||||
import { NativeDmgArtifactReader } from "../NativeDmgArtifactReader.js";
|
||||
@@ -7,7 +10,6 @@ import { ZipArtifactReader } from "../ZipArtifactReader.js";
|
||||
import type {
|
||||
ArtifactInventoryCoverage,
|
||||
ArtifactOccurrence,
|
||||
IntegrityContradiction,
|
||||
} from "../../domain/artifactGraph.js";
|
||||
import {
|
||||
isZipPackageFormat,
|
||||
@@ -40,42 +42,47 @@ export const createReader = (
|
||||
}
|
||||
};
|
||||
|
||||
/** Classify source coverage and preserve its user-facing explanation once. */
|
||||
export const inventoryAssessment = (
|
||||
/** Decide traversal scope and availability once, independently of diagnostic wording. */
|
||||
export const inventoryCoverage = (
|
||||
format: ArtifactOccurrence["artifact_format"],
|
||||
reader: ArtifactReader | undefined,
|
||||
integrityContradictions: readonly IntegrityContradiction[],
|
||||
caseCollisionLimitation: string | undefined,
|
||||
traversal: ArtifactReaderTraversal,
|
||||
): {
|
||||
readonly coverage_status: ArtifactInventoryCoverage;
|
||||
readonly coverage: ArtifactInventoryCoverage;
|
||||
readonly limitations: string[];
|
||||
} => {
|
||||
let coverageStatus: ArtifactInventoryCoverage = "complete";
|
||||
const limitations: string[] = [];
|
||||
if (reader !== undefined) {
|
||||
if (isZipPackageFormat(reader.format))
|
||||
limitations.push(ZIP_NON_ENTRY_TAIL_LIMITATION);
|
||||
} else if (format === "dmg" || format === "pkg") {
|
||||
coverageStatus = "partial";
|
||||
limitations.push(
|
||||
`${format.toUpperCase()} root hash is observed; child inventory requires a native macOS adapter.`,
|
||||
);
|
||||
} else if (format === "mach-o-universal" && process.platform !== "darwin") {
|
||||
coverageStatus = "partial";
|
||||
limitations.push(
|
||||
"Universal Mach-O slices require the native macOS lipo adapter.",
|
||||
);
|
||||
} else {
|
||||
// The graph's modeled scope is the root itself for recognized leaf formats.
|
||||
limitations.push("Artifact has no child container entries.");
|
||||
}
|
||||
if (caseCollisionLimitation !== undefined) {
|
||||
coverageStatus = "partial";
|
||||
limitations.push(caseCollisionLimitation);
|
||||
}
|
||||
if (integrityContradictions.length > 0)
|
||||
limitations.push(
|
||||
`${String(integrityContradictions.length)} integrity contradiction(s) were recorded; mismatched content is observed-untrusted.`,
|
||||
);
|
||||
return { coverage_status: coverageStatus, limitations };
|
||||
if (reader !== undefined)
|
||||
return {
|
||||
coverage: { scope: "container-entries", status: traversal.status },
|
||||
limitations: [
|
||||
...(isZipPackageFormat(reader.format)
|
||||
? [ZIP_NON_ENTRY_TAIL_LIMITATION]
|
||||
: []),
|
||||
...traversal.limitations,
|
||||
],
|
||||
};
|
||||
// dmg and pkg reach here only after classifyRootFormat matched their bytes.
|
||||
if (format === "dmg" || format === "pkg")
|
||||
return {
|
||||
coverage: { scope: "container-entries", status: "unknown" },
|
||||
limitations: [
|
||||
`${format.toUpperCase()} root hash is observed; child inventory requires a native macOS adapter.`,
|
||||
],
|
||||
};
|
||||
if (format === "mach-o-universal")
|
||||
return {
|
||||
coverage: { scope: "container-entries", status: "unknown" },
|
||||
limitations: [
|
||||
"Universal Mach-O slices require the native macOS lipo adapter.",
|
||||
],
|
||||
};
|
||||
if (isZipPackageFormat(format) || format === "directory" || format === "asar")
|
||||
return {
|
||||
coverage: { scope: "container-entries", status: "unknown" },
|
||||
limitations: [`${format} child inventory reader is unavailable.`],
|
||||
};
|
||||
return {
|
||||
coverage: { scope: "root", status: "complete" },
|
||||
limitations: ["Artifact has no child container entries."],
|
||||
};
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import type { RootInventorySource } from "./classify.js";
|
||||
import {
|
||||
ArtifactReaderFailure,
|
||||
type ArtifactReader,
|
||||
type ArtifactReaderTraversal,
|
||||
} from "../ArtifactReader.js";
|
||||
import type { ArtifactResourceOwner } from "../ArtifactResourceScope.js";
|
||||
import {
|
||||
@@ -40,7 +41,7 @@ import {
|
||||
hashStableRootArtifact,
|
||||
hashStableRootArtifactHandle,
|
||||
} from "./hashStableRootArtifact.js";
|
||||
import { createReader, inventoryAssessment } from "./reader.js";
|
||||
import { createReader, inventoryCoverage } from "./reader.js";
|
||||
import {
|
||||
scanReader,
|
||||
type PendingIntegrityContradiction,
|
||||
@@ -99,6 +100,7 @@ export const scanCanonicalArtifactInventoryInScope = async (
|
||||
occurrences,
|
||||
pendingContradictions,
|
||||
caseCollisionLimitation,
|
||||
traversal,
|
||||
} = await scanReader(reader, ownedReaders, options.signal, integrity);
|
||||
outcome = {
|
||||
kind: "completed",
|
||||
@@ -115,6 +117,7 @@ export const scanCanonicalArtifactInventoryInScope = async (
|
||||
occurrences,
|
||||
pendingContradictions,
|
||||
caseCollisionLimitation,
|
||||
traversal,
|
||||
}),
|
||||
};
|
||||
} catch (cause: unknown) {
|
||||
@@ -196,6 +199,7 @@ interface SnapshotBuildInput {
|
||||
readonly occurrences: MutableOccurrence[];
|
||||
readonly pendingContradictions: PendingIntegrityContradiction[];
|
||||
readonly caseCollisionLimitation: string | undefined;
|
||||
readonly traversal: ArtifactReaderTraversal;
|
||||
}
|
||||
|
||||
const buildInventorySnapshot = async (
|
||||
@@ -268,12 +272,7 @@ const buildInventorySnapshot = async (
|
||||
orderedOccurrences,
|
||||
orderedEdges,
|
||||
});
|
||||
const assessment = inventoryAssessment(
|
||||
rootFormat,
|
||||
reader,
|
||||
integrityContradictions,
|
||||
input.caseCollisionLimitation,
|
||||
);
|
||||
const traversal = inventoryCoverage(rootFormat, reader, input.traversal);
|
||||
return {
|
||||
manifest,
|
||||
nodes: orderedNodes,
|
||||
@@ -281,8 +280,14 @@ const buildInventorySnapshot = async (
|
||||
edges: orderedEdges,
|
||||
provenance: reader?.provenance() ?? [],
|
||||
integrity_contradictions: integrityContradictions,
|
||||
coverage_status: assessment.coverage_status,
|
||||
limitations: assessment.limitations,
|
||||
coverage: traversal.coverage,
|
||||
coverage_status:
|
||||
traversal.coverage.status === "complete" ? "complete" : "partial",
|
||||
limitations: buildLimitations(
|
||||
traversal.limitations,
|
||||
integrityContradictions,
|
||||
input.caseCollisionLimitation,
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -395,3 +400,17 @@ const buildManifest = ({
|
||||
occurrence_count: orderedOccurrences.length,
|
||||
edge_count: orderedEdges.length,
|
||||
});
|
||||
|
||||
const buildLimitations = (
|
||||
scopeLimitations: readonly string[],
|
||||
integrityContradictions: readonly IntegrityContradiction[],
|
||||
caseCollisionLimitation: string | undefined,
|
||||
): string[] => [
|
||||
...scopeLimitations,
|
||||
...(caseCollisionLimitation === undefined ? [] : [caseCollisionLimitation]),
|
||||
...(integrityContradictions.length === 0
|
||||
? []
|
||||
: [
|
||||
`${String(integrityContradictions.length)} integrity contradiction(s) were recorded; mismatched content is observed-untrusted.`,
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -9,9 +9,12 @@ import {
|
||||
ArtifactReaderFailure,
|
||||
type ArtifactEntry,
|
||||
type ArtifactReader,
|
||||
type ArtifactReaderTraversal,
|
||||
type ArtifactTraversalOmission,
|
||||
} from "../ArtifactReader.js";
|
||||
import { AsarArtifactReader } from "../AsarArtifactReader.js";
|
||||
import type { ArtifactNode } from "../../domain/artifactGraph.js";
|
||||
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
|
||||
import {
|
||||
classifyArtifactContent,
|
||||
classifyArtifactBytes,
|
||||
@@ -55,6 +58,7 @@ interface ScanContext {
|
||||
readonly registry: ArtifactPathRegistry;
|
||||
readonly expandedContainerIds: Set<string>;
|
||||
readonly ownedReaders: ArtifactReader[];
|
||||
readonly omissions: ArtifactTraversalOmission[];
|
||||
}
|
||||
|
||||
export const scanReader = async (
|
||||
@@ -67,6 +71,7 @@ export const scanReader = async (
|
||||
readonly occurrences: MutableOccurrence[];
|
||||
readonly pendingContradictions: PendingIntegrityContradiction[];
|
||||
readonly caseCollisionLimitation: string | undefined;
|
||||
readonly traversal: ArtifactReaderTraversal;
|
||||
}> => {
|
||||
const { nodes, occurrences, pendingContradictions } = emptyScan();
|
||||
if (reader === undefined)
|
||||
@@ -75,6 +80,7 @@ export const scanReader = async (
|
||||
occurrences,
|
||||
pendingContradictions,
|
||||
caseCollisionLimitation: undefined,
|
||||
traversal: { status: "complete", limitations: [] },
|
||||
};
|
||||
const context: ScanContext = {
|
||||
reader,
|
||||
@@ -87,6 +93,7 @@ export const scanReader = async (
|
||||
registry: new ArtifactPathRegistry(),
|
||||
expandedContainerIds: new Set<string>(),
|
||||
ownedReaders,
|
||||
omissions: [],
|
||||
};
|
||||
await visitArtifactEntries(context, reader, "");
|
||||
// Archive directories may appear after their children. Resolve containment
|
||||
@@ -101,9 +108,37 @@ export const scanReader = async (
|
||||
occurrences,
|
||||
pendingContradictions,
|
||||
caseCollisionLimitation: annotateEnglishUnicodeCaseCollisions(occurrences),
|
||||
traversal: {
|
||||
status: context.omissions.length === 0 ? "complete" : "unknown",
|
||||
limitations: [
|
||||
...new Set(
|
||||
context.omissions.map(
|
||||
({ path, kind }) =>
|
||||
`Artifact traversal omitted ${kind} at ${JSON.stringify(path)}.`,
|
||||
),
|
||||
),
|
||||
].sort(compareUnicodeCodePoints),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const inventoryEntries = (
|
||||
context: ScanContext,
|
||||
reader: ArtifactReader,
|
||||
prefix: string,
|
||||
): AsyncIterator<ArtifactEntry> =>
|
||||
reader
|
||||
.entries(context.signal, {
|
||||
onOmission: (omission) =>
|
||||
context.omissions.push({
|
||||
...omission,
|
||||
path: normalizeArtifactPath(
|
||||
prefix.length === 0 ? omission.path : `${prefix}/${omission.path}`,
|
||||
),
|
||||
}),
|
||||
})
|
||||
[Symbol.asyncIterator]();
|
||||
|
||||
const visitArtifactEntries = async (
|
||||
context: ScanContext,
|
||||
currentReader: ArtifactReader,
|
||||
@@ -117,7 +152,7 @@ const visitArtifactEntries = async (
|
||||
{
|
||||
reader: currentReader,
|
||||
prefix,
|
||||
iterator: currentReader.entries(context.signal)[Symbol.asyncIterator](),
|
||||
iterator: inventoryEntries(context, currentReader, prefix),
|
||||
},
|
||||
];
|
||||
let failure: { readonly cause: unknown } | undefined;
|
||||
@@ -191,7 +226,7 @@ const visitArtifactEntries = async (
|
||||
stack.push({
|
||||
reader: nested,
|
||||
prefix: logicalPath,
|
||||
iterator: nested.entries(context.signal)[Symbol.asyncIterator](),
|
||||
iterator: inventoryEntries(context, nested, logicalPath),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { InspectWebPageInput } from "../domain/browserObservation.js";
|
||||
import type { WebPageInspection } from "../domain/browserObservationSchemas.js";
|
||||
import type { BrowserExclusionReason } from "../domain/browserCompleteness.js";
|
||||
import { CdpConnection } from "./CdpConnection.js";
|
||||
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
|
||||
import {
|
||||
@@ -26,6 +27,7 @@ export const captureStorage = async (
|
||||
limitations: string[],
|
||||
): Promise<{
|
||||
readonly value: WebPageInspection["storage"];
|
||||
readonly keysExclusion: BrowserExclusionReason | undefined;
|
||||
}> => {
|
||||
const origin = new URL(pageUrl).origin;
|
||||
const quota = recordValue(
|
||||
@@ -82,7 +84,18 @@ export const captureStorage = async (
|
||||
// list is an incomplete enumeration rather than an empty one.
|
||||
Array.isArray(cacheField) &&
|
||||
caches.length === cacheField.length;
|
||||
const keysExclusion = !context.input.include_storage_keys
|
||||
? undefined
|
||||
: local.exclusion === "provider_unavailable" ||
|
||||
session.exclusion === "provider_unavailable" ||
|
||||
indexedRaw === undefined ||
|
||||
cacheRaw === undefined
|
||||
? "provider_unavailable"
|
||||
: !local.complete || !session.complete || !structuredStorageComplete
|
||||
? "invalid_protocol_value"
|
||||
: undefined;
|
||||
return {
|
||||
keysExclusion,
|
||||
value: {
|
||||
origin,
|
||||
usage_bytes: numberValue(quota?.usage) ?? null,
|
||||
@@ -99,7 +112,11 @@ export const captureStorage = async (
|
||||
};
|
||||
};
|
||||
|
||||
const emptyStorageItems: CapturedStorageItems = {
|
||||
type StorageItemsCapture = CapturedStorageItems & {
|
||||
readonly exclusion?: "provider_unavailable" | "invalid_protocol_value";
|
||||
};
|
||||
|
||||
const emptyStorageItems: StorageItemsCapture = {
|
||||
items: [],
|
||||
complete: false,
|
||||
};
|
||||
@@ -118,7 +135,7 @@ const storageItems = async (
|
||||
origin: string,
|
||||
isLocalStorage: boolean,
|
||||
limitations: string[],
|
||||
): Promise<CapturedStorageItems> => {
|
||||
): Promise<StorageItemsCapture> => {
|
||||
const raw = await optionalCdpCommand(
|
||||
context,
|
||||
"DOMStorage.getDOMStorageItems",
|
||||
@@ -127,7 +144,12 @@ const storageItems = async (
|
||||
);
|
||||
const entries = recordValue(raw)?.entries;
|
||||
if (raw === undefined || !Array.isArray(entries))
|
||||
return { items: [], complete: false };
|
||||
return {
|
||||
items: [],
|
||||
complete: false,
|
||||
exclusion:
|
||||
raw === undefined ? "provider_unavailable" : "invalid_protocol_value",
|
||||
};
|
||||
const items = entries.flatMap((entry) => {
|
||||
if (!Array.isArray(entry)) return [];
|
||||
const key = cdpStringValue(entry[0]);
|
||||
|
||||
@@ -193,7 +193,9 @@ const addIndexedDb = async (
|
||||
scope: "indexed_db_schema",
|
||||
identity: databaseName,
|
||||
value: stores.map(indexedDbStoreSchema),
|
||||
complete: indexedDbSchemaComplete(stores),
|
||||
complete:
|
||||
stores.length === database.objectStores.length &&
|
||||
indexedDbSchemaComplete(stores),
|
||||
});
|
||||
for (const store of stores) {
|
||||
const objectStoreName = cdpStringValue(store.name);
|
||||
@@ -228,19 +230,37 @@ const indexedDbSchemaComplete = (
|
||||
stores.every(
|
||||
(store) =>
|
||||
cdpStringValue(store.name) !== undefined &&
|
||||
stableValueComplete(store.keyPath) &&
|
||||
indexedDbKeyPathComplete(store.keyPath) &&
|
||||
typeof store.autoIncrement === "boolean" &&
|
||||
Array.isArray(store.indexes) &&
|
||||
recordsValue(store.indexes).length === store.indexes.length &&
|
||||
recordsValue(store.indexes).every(
|
||||
(index) =>
|
||||
cdpStringValue(index.name) !== undefined &&
|
||||
stableValueComplete(index.keyPath) &&
|
||||
indexedDbKeyPathComplete(index.keyPath) &&
|
||||
typeof index.unique === "boolean" &&
|
||||
typeof index.multiEntry === "boolean",
|
||||
),
|
||||
);
|
||||
|
||||
const indexedDbKeyPathComplete = (value: unknown): boolean => {
|
||||
const keyPath = recordValue(value);
|
||||
if (keyPath === undefined) return false;
|
||||
switch (keyPath.type) {
|
||||
case "null":
|
||||
return true;
|
||||
case "string":
|
||||
return typeof keyPath.string === "string";
|
||||
case "array":
|
||||
return (
|
||||
Array.isArray(keyPath.array) &&
|
||||
keyPath.array.every((key) => typeof key === "string")
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const addIndexedDbRecords = async (
|
||||
run: FingerprintRun,
|
||||
input: {
|
||||
@@ -474,14 +494,49 @@ const stableValueComplete = (value: unknown): boolean => {
|
||||
|
||||
const remoteObjectComplete = (value: unknown): boolean => {
|
||||
const record = recordValue(value);
|
||||
if (record === undefined) return true;
|
||||
return (
|
||||
record.objectId === undefined &&
|
||||
record.preview === undefined &&
|
||||
record.customPreview === undefined &&
|
||||
(record.type !== "object" || record.value !== undefined) &&
|
||||
stableValueComplete(record)
|
||||
);
|
||||
if (
|
||||
record === undefined ||
|
||||
record.objectId !== undefined ||
|
||||
record.preview !== undefined ||
|
||||
record.customPreview !== undefined ||
|
||||
!stableValueComplete(record)
|
||||
)
|
||||
return false;
|
||||
switch (record.type) {
|
||||
case "undefined":
|
||||
return (
|
||||
record.value === undefined && record.unserializableValue === undefined
|
||||
);
|
||||
case "string":
|
||||
case "boolean":
|
||||
return (
|
||||
typeof record.value === record.type &&
|
||||
record.unserializableValue === undefined
|
||||
);
|
||||
case "number":
|
||||
return record.unserializableValue === undefined
|
||||
? typeof record.value === "number" && Number.isFinite(record.value)
|
||||
: record.value === undefined &&
|
||||
typeof record.unserializableValue === "string" &&
|
||||
["-0", "NaN", "Infinity", "-Infinity"].includes(
|
||||
record.unserializableValue,
|
||||
);
|
||||
case "bigint":
|
||||
return (
|
||||
record.value === undefined &&
|
||||
typeof record.unserializableValue === "string" &&
|
||||
/^-?\d+n$/u.test(record.unserializableValue)
|
||||
);
|
||||
case "object":
|
||||
return (
|
||||
record.unserializableValue === undefined &&
|
||||
(record.subtype === "null"
|
||||
? record.value === null
|
||||
: record.value !== null && typeof record.value === "object")
|
||||
);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const decodeBase64 = (value: string): Buffer | null => {
|
||||
|
||||
@@ -155,6 +155,12 @@ const captureAuthorizedPage = async (
|
||||
);
|
||||
if (!input.include_storage_keys)
|
||||
state.events.completeness.exclude("storage_keys", "not_approved", null);
|
||||
else if (storageCapture.keysExclusion !== undefined)
|
||||
state.events.completeness.exclude(
|
||||
"storage_keys",
|
||||
storageCapture.keysExclusion,
|
||||
null,
|
||||
);
|
||||
await report(context.progress, 3, "Normalizing browser evidence");
|
||||
const completedFrameResult = await authorizedMainFrame({
|
||||
connection: context.connection,
|
||||
|
||||
@@ -186,6 +186,21 @@ it("excludes structural auth material and explicit normalized values without gue
|
||||
expect(traces[0]?.incomplete_fact_records).toContain(2);
|
||||
});
|
||||
|
||||
it("omits a hidden derived total for a stream with more contributions than call arguments allow", () => {
|
||||
const events = Array.from({ length: 130 }, (_, index) =>
|
||||
event(
|
||||
Array.from({ length: 1_000 }, (_, offset) =>
|
||||
frame(index * 1_000 + offset, 1),
|
||||
),
|
||||
),
|
||||
);
|
||||
const { traces } = facts(capture(events), ["130000"]);
|
||||
expect(traces[0]?.streams).toEqual([]);
|
||||
expect(traces[0]?.incomplete_fact_records).toEqual(
|
||||
Array.from({ length: 130 }, (_, ordinal) => ordinal),
|
||||
);
|
||||
});
|
||||
|
||||
it("hides marked ancestor keys and safe derived sums while still validating hidden subtrees", () => {
|
||||
const text = capture([event([frame(0, 3), frame(3, 4)])]);
|
||||
expect(facts(text, ["7"]).traces[0]?.streams).toEqual([]);
|
||||
|
||||
@@ -95,10 +95,10 @@ export const decodeQlogCapture = (
|
||||
...stream.reported_final_sizes,
|
||||
...stream.covered_ranges.flatMap((r) => [r.start, r.end]),
|
||||
];
|
||||
// A stream can have more contributions than call arguments allow.
|
||||
if (numericValues.some(excluded))
|
||||
fact.incomplete_fact_records.push(
|
||||
...stream.contributions.map((c) => c.record_ordinal),
|
||||
);
|
||||
for (const contribution of stream.contributions)
|
||||
fact.incomplete_fact_records.push(contribution.record_ordinal);
|
||||
else fact.streams.push(stream);
|
||||
}
|
||||
fact.incomplete_fact_records = [...new Set(fact.incomplete_fact_records)];
|
||||
|
||||
@@ -5,6 +5,7 @@ import { snapshotEnvironment } from "./process/snapshotEnvironment.js";
|
||||
import { createDirectAnalysis } from "./composition/directAnalysis.js";
|
||||
import { registerEvmCommands } from "./cli/evmCommands.js";
|
||||
import { registerGoCommands } from "./cli/goCommands.js";
|
||||
import { registerRustCommands } from "./cli/rustCommands.js";
|
||||
import { registerSqliteCommands } from "./cli/sqliteCommands.js";
|
||||
import { registerAnalysisViewCommands } from "./cli/analysisViewCommands.js";
|
||||
import { registerBinaryDiagnosticsCommands } from "./cli/binaryDiagnosticsCommands.js";
|
||||
@@ -101,6 +102,7 @@ export const createCli = (
|
||||
registerSignalCommands(cli, logger, environment);
|
||||
registerEvmCommands(cli, logger, environment);
|
||||
registerGoCommands(cli, logger);
|
||||
registerRustCommands(cli, logger);
|
||||
registerSqliteCommands(cli, logger, environment);
|
||||
registerArtifactCommands(cli, logger, analysis.runProviderAnalysis);
|
||||
registerManagedCommands(cli, logger, analysis.runProviderAnalysis);
|
||||
|
||||
@@ -56,13 +56,13 @@ const registerInspectManagedMembers = (
|
||||
): void => {
|
||||
cli.command(CLI_COMMANDS.inspectManagedMembers, {
|
||||
description:
|
||||
"Inspect PE/CLI metadata members, signatures, and CIL anchors without loading target code",
|
||||
"Inspect PE/CLI metadata members, signatures, CIL anchors, and ldstr string literals without loading target code",
|
||||
args: z.object({
|
||||
path: z.string().describe("Managed PE executable or assembly path"),
|
||||
}),
|
||||
options: z.object({
|
||||
type: managedMembersInputSchema.shape.type_names.describe(
|
||||
"Inspect only members declared by this exact type full name, such as System.Threading.Monitor; a nested type is named by its bare name, which selects every nested type of that name (repeatable)",
|
||||
"Inspect only members declared by this exact type full name, and the string literals they load, such as System.Threading.Monitor; a nested type is named by its bare name, which selects every nested type of that name (repeatable)",
|
||||
),
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from "incur";
|
||||
import { resolve } from "node:path";
|
||||
import { createRustBinaryService } from "../composition/rust.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
|
||||
/** Resolve caller paths and inspect embedded Rust dependencies through the shared workflow. */
|
||||
export const registerRustCommands = (
|
||||
cli: CliInstance,
|
||||
logger: Logger,
|
||||
): void => {
|
||||
const service = createRustBinaryService();
|
||||
cli.command(CLI_COMMANDS.inspectRustBinary, {
|
||||
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"),
|
||||
}),
|
||||
run: ({ args }) =>
|
||||
withCommandCancellation((signal) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.inspectRustBinary, async () => {
|
||||
const result = await service.inspect(
|
||||
{ path: resolve(args.path) },
|
||||
{ signal },
|
||||
);
|
||||
return result.ok ? result.value : projectAnalysisError(result.error);
|
||||
}),
|
||||
),
|
||||
});
|
||||
};
|
||||
@@ -50,6 +50,7 @@ export const CLI_COMMANDS = Object.freeze({
|
||||
inspectRecordedCrash: "inspect-recorded-crash",
|
||||
inspectBinaryLayout: "inspect-binary-layout",
|
||||
inspectGoBinary: "inspect-go-binary",
|
||||
inspectRustBinary: "inspect-rust-binary",
|
||||
inspectPeResources: "inspect-pe-resources",
|
||||
inspectAnalysisView: "inspect-analysis-view",
|
||||
listFridaDevices: "list-frida-devices",
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { RustBinaryService } from "../application/rust/RustBinaryService.js";
|
||||
import { RustBinaryProvider } from "../rust/RustBinaryProvider.js";
|
||||
|
||||
/** Compose bundled offline inspection without a Rust toolchain or any child process. */
|
||||
export const createRustBinaryService = (): RustBinaryService =>
|
||||
new RustBinaryService(new RustBinaryProvider());
|
||||
@@ -249,6 +249,40 @@ describe("configuration failure diagnostics", () => {
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["1", "90000", "2147483647"])(
|
||||
"accepts Swift compile deadline %s without changing Hopper's deadline",
|
||||
(value) => {
|
||||
expect(
|
||||
parseConfig({ REA_SWIFTC_COMPILE_TIMEOUT_MS: value }),
|
||||
).toMatchObject({
|
||||
ok: true,
|
||||
value: {
|
||||
hopperStartupTimeoutMs: 120_000,
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["", "0", "-1", "1.5", "1e3", "invalid", "2147483648"])(
|
||||
"rejects malformed Swift compile deadline %j with its setting",
|
||||
(value) => {
|
||||
const result = parseConfig({ REA_SWIFTC_COMPILE_TIMEOUT_MS: value });
|
||||
if (result.ok) throw new Error("Expected invalid Swift compile deadline");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
category: "invalid_input",
|
||||
details: {
|
||||
settings: [
|
||||
{
|
||||
setting: "REA_SWIFTC_COMPILE_TIMEOUT_MS",
|
||||
constraint: expect.any(String),
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["GHIDRA_INSTALL_DIR", "./fixture"],
|
||||
["JAVA_HOME", "./jdk"],
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ghidraDisabledAnalyzersSchema } from "./ghidraDisabledAnalyzers.js";
|
||||
import {
|
||||
DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS,
|
||||
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
|
||||
DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS,
|
||||
providerStartupTimeoutSchema,
|
||||
} from "./providerStartupTimeout.js";
|
||||
import { mcpInputSchemaProfileSchema } from "./mcpInputSchemaProfile.js";
|
||||
@@ -47,6 +48,9 @@ const environmentSchema = z.object({
|
||||
REA_HOPPER_STARTUP_TIMEOUT_MS: providerStartupTimeoutSchema.default(
|
||||
DEFAULT_HOPPER_STARTUP_TIMEOUT_MS,
|
||||
),
|
||||
REA_SWIFTC_COMPILE_TIMEOUT_MS: providerStartupTimeoutSchema.default(
|
||||
DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS,
|
||||
),
|
||||
REA_GHIDRA_LANGUAGE_ID: ghidraLanguageIdSchema.optional(),
|
||||
REA_GHIDRA_COMPILER_SPEC_ID: ghidraCompilerSpecIdSchema.optional(),
|
||||
REA_GHIDRA_DISABLED_ANALYZERS: ghidraDisabledAnalyzersSchema.optional(),
|
||||
|
||||
@@ -6,6 +6,10 @@ export const DEFAULT_GHIDRA_STARTUP_TIMEOUT_MS = 330_000;
|
||||
/** Default absolute deadline for Hopper launch, analysis, bridge, and readiness. */
|
||||
export const DEFAULT_HOPPER_STARTUP_TIMEOUT_MS = 120_000;
|
||||
|
||||
/** Default deadline for Darwin ownership-helper `swiftc` compilation. */
|
||||
/** Fresh Swift SDK cache took ~88s on the supported Intel macOS VM. */
|
||||
export const DEFAULT_SWIFTC_COMPILE_TIMEOUT_MS = 120_000;
|
||||
|
||||
// Node schedules delays above this signed 32-bit boundary as a 1 ms timer.
|
||||
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ export const managedMembersInputSchema = managedTargetInputSchema.extend({
|
||||
})
|
||||
.meta({ uniqueItems: true })
|
||||
.describe(
|
||||
"Inspect only the fields, methods, call edges, and field accesses declared by these exact type full names; omit to inspect every type. Unmatched names are reported in selection.unmatched_type_names.",
|
||||
"Inspect only the fields, methods, call edges, and field accesses declared by these exact type full names, and the user strings their method bodies load; omit to inspect every type. Unmatched names are reported in selection.unmatched_type_names.",
|
||||
)
|
||||
.optional(),
|
||||
});
|
||||
@@ -87,7 +87,7 @@ export const MANAGED_TOOL_CONTRACTS = [
|
||||
name: "inspect_managed_members",
|
||||
...toolContractMetadata("inspect_managed_members"),
|
||||
description:
|
||||
"Inspect PE/CLI metadata members, signatures, raw CIL hashes, decoded-instruction-tuple hashes, separately reported exception regions, call edges, and field-access anchors without loading or executing target code. Select type_names to decode only the members of those exact declaring types, which keeps large assemblies such as a base class library within the representation budget. Metadata tokens are reported as build-local coordinates bound to the artifact SHA-256 and MVID; the tuple hash does not resolve them or fully commit control flow.",
|
||||
"Inspect PE/CLI metadata members, signatures, raw CIL hashes, decoded-instruction-tuple hashes, separately reported exception regions, call edges, field-access anchors, and the #US string literals that ldstr loads, without loading or executing target code. Select type_names to decode only the members of those exact declaring types and the literals their bodies load, which keeps large assemblies such as a base class library within the representation budget. Metadata tokens are reported as build-local coordinates bound to the artifact SHA-256 and MVID; the tuple hash does not resolve them or fully commit control flow.",
|
||||
kind: "managed-provider",
|
||||
inputSchema: managedMembersInputSchema,
|
||||
outputSchema: memberOutputSchema,
|
||||
|
||||
@@ -53,6 +53,7 @@ const emptyManagedMembers = (
|
||||
member_refs: [],
|
||||
call_edges: [],
|
||||
field_accesses: [],
|
||||
user_strings: { state: "complete", literals: [], issues: [] },
|
||||
selection: null,
|
||||
coverage: { state: "complete", issues: [] },
|
||||
limitations: [],
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
inspectRustBinaryInputSchema,
|
||||
rustBinarySchema,
|
||||
} from "../../domain/rust/rustBinary.js";
|
||||
import type { ToolContract } from "../toolContractTypes.js";
|
||||
import { toolContractMetadata } from "../toolEffects.js";
|
||||
import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
|
||||
|
||||
/** Static Rust dependency metadata is independent of active native-analysis providers. */
|
||||
export const RUST_TOOL_CONTRACTS = [
|
||||
{
|
||||
name: "inspect_rust_binary",
|
||||
...toolContractMetadata("inspect_rust_binary"),
|
||||
kind: "artifact-provider",
|
||||
description:
|
||||
"Inspect the cargo-auditable crate dependency list embedded in an explicit local ELF, PE or thin Mach-O image. Returns the artifact SHA-256, the .dep-v0 section file offset and size, compressed and uncompressed sizes, format version, packages in their original order, the root package index, and dependency edges by index. Reads zlib-compressed JSON without installing Rust, opening a disassembler, executing the target, writing temporary files or accessing the network. A missing section is null metadata with a limitation and is not proof that the binary has no dependencies. A rustc commit hash is reported only as an inference when a /rustc/<40-hex>/ path is present in read-only data. Resource guards bound files to 256 MiB, dependency sections to 1 MiB, inflated JSON to the smaller of 8 MiB and 64 times the compressed size, and the complete report to 8 MiB. Malformed, unsupported and oversized inputs receive typed failures rather than partial success.",
|
||||
inputSchema: inspectRustBinaryInputSchema,
|
||||
outputSchema: evidenceResultOf(rustBinarySchema),
|
||||
examples: [
|
||||
{
|
||||
title: "Inspect an embedded Rust dependency list",
|
||||
input: { path: "/artifacts/application" },
|
||||
},
|
||||
],
|
||||
},
|
||||
] as const satisfies readonly ToolContract[];
|
||||
@@ -3,6 +3,7 @@ import { SIGNAL_TOOL_CONTRACTS } from "./signals/signalToolContracts.js";
|
||||
import { WASM_TOOL_CONTRACTS } from "./wasm/wasmToolContracts.js";
|
||||
import { EVM_TOOL_CONTRACTS } from "./evm/evmToolContracts.js";
|
||||
import { GO_TOOL_CONTRACTS } from "./go/goToolContracts.js";
|
||||
import { RUST_TOOL_CONTRACTS } from "./rust/rustToolContracts.js";
|
||||
import { SQLITE_TOOL_CONTRACTS } from "./sqlite/sqliteToolContracts.js";
|
||||
import { ANALYSIS_VIEW_TOOL_CONTRACTS } from "./analysisViewToolContracts.js";
|
||||
import { BINARY_DIAGNOSTICS_TOOL_CONTRACTS } from "./native/binaryDiagnosticsToolContracts.js";
|
||||
@@ -40,6 +41,7 @@ export const TOOL_CONTRACTS = [
|
||||
...ARTIFACT_TOOL_CONTRACTS,
|
||||
...EVM_TOOL_CONTRACTS,
|
||||
...GO_TOOL_CONTRACTS,
|
||||
...RUST_TOOL_CONTRACTS,
|
||||
...WASM_TOOL_CONTRACTS,
|
||||
...SQLITE_TOOL_CONTRACTS,
|
||||
...CAPTURE_TOOL_CONTRACTS,
|
||||
|
||||
@@ -19,6 +19,8 @@ const EXECUTION_WORDING_EXCEPTIONS: Readonly<Record<string, string>> = {
|
||||
resolve_native_call_targets: "states that it does not execute code",
|
||||
inspect_pe_resources: "states that it does not execute the image",
|
||||
inspect_go_binary: "reads embedded metadata without executing the target",
|
||||
inspect_rust_binary:
|
||||
"reads an embedded dependency list without executing the target",
|
||||
inspect_sqlite_database:
|
||||
"uses a trusted inspector worker without executing application code or caller SQL",
|
||||
inspect_binary_layout:
|
||||
|
||||
@@ -54,6 +54,7 @@ const sessionEvidence = effects({ mutatesSession: true });
|
||||
/** Explicit effect audit for every public tool. */
|
||||
export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
|
||||
inspect_go_binary: evidence,
|
||||
inspect_rust_binary: evidence,
|
||||
inspect_sqlite_database: effects({
|
||||
mutatesSession: true,
|
||||
writesFilesystem: true,
|
||||
|
||||
@@ -68,7 +68,7 @@ export const androidApplicationProjectionResultSchema = z.strictObject({
|
||||
// Count completeness is separate from the declared coverage status.
|
||||
inventory_complete: z.boolean(),
|
||||
}),
|
||||
limitations: z.array(z.string().min(1)),
|
||||
limitations: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type AndroidApplicationProjectionInput = z.infer<
|
||||
@@ -99,7 +99,8 @@ export const projectAndroidApplication = (
|
||||
);
|
||||
const components = classified;
|
||||
const limitations = [
|
||||
...(!hasCompleteInventoryCoverage(inventory)
|
||||
...inventory.limitations,
|
||||
...(!hasCompleteInventoryCoverage(inventory, "container-entries")
|
||||
? ["Source inventory coverage is partial; absence is unknown."]
|
||||
: []),
|
||||
"Manifest, resource, signing, and bytecode semantics require a dedicated Android provider; this projection reports exact inventory paths and hashes only.",
|
||||
|
||||
@@ -94,7 +94,7 @@ export const appleApplicationProjectionResultSchema = z.strictObject({
|
||||
// Count completeness is separate from the declared coverage status.
|
||||
inventory_complete: z.boolean(),
|
||||
}),
|
||||
limitations: z.array(z.string().min(1)),
|
||||
limitations: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type AppleApplicationProjectionInput = z.infer<
|
||||
@@ -169,18 +169,24 @@ export const projectAppleApplication = (
|
||||
);
|
||||
const uninventoriedImage =
|
||||
rootFormat === "dmg" && inventory.occurrences.length === 1;
|
||||
const inventoryComplete = hasCompleteInventoryCoverage(inventory);
|
||||
const inventoryComplete = hasCompleteInventoryCoverage(
|
||||
inventory,
|
||||
"container-entries",
|
||||
);
|
||||
const complete = inventoryComplete && !uninventoriedImage;
|
||||
const limitations = projectionLimitations({
|
||||
complete: inventoryComplete,
|
||||
uninventoriedImage,
|
||||
roots,
|
||||
bundles,
|
||||
sidecars: entries.some(({ path }) => isSidecar(path)),
|
||||
symlinks: entries.some(({ kind }) => kind === "symlink"),
|
||||
unattributed,
|
||||
bridgeCandidateCoverage: bridgeProjection.coverage,
|
||||
});
|
||||
const limitations = [
|
||||
...inventory.limitations,
|
||||
...projectionLimitations({
|
||||
complete: inventoryComplete,
|
||||
uninventoriedImage,
|
||||
roots,
|
||||
bundles,
|
||||
sidecars: entries.some(({ path }) => isSidecar(path)),
|
||||
symlinks: entries.some(({ kind }) => kind === "symlink"),
|
||||
unattributed,
|
||||
bridgeCandidateCoverage: bridgeProjection.coverage,
|
||||
}),
|
||||
];
|
||||
const withoutId = {
|
||||
root_sha256: inventory.manifest.root_sha256,
|
||||
root_format: rootFormat,
|
||||
|
||||
@@ -104,13 +104,51 @@ export const compareArtifacts = (
|
||||
const right = parseArtifactInventoryEvidence(rightEvidence);
|
||||
const leftComplete = left.inventory.complete;
|
||||
const rightComplete = right.inventory.complete;
|
||||
const leftCovered = hasCompleteInventoryCoverage(left.inventory);
|
||||
const rightCovered = hasCompleteInventoryCoverage(right.inventory);
|
||||
const sameScope =
|
||||
left.inventory.coverage.scope === right.inventory.coverage.scope;
|
||||
const rootCoverageComplete =
|
||||
sameScope &&
|
||||
left.inventory.complete &&
|
||||
right.inventory.complete &&
|
||||
left.inventory.coverage.status === "complete" &&
|
||||
right.inventory.coverage.status === "complete";
|
||||
const entryCoverageComplete =
|
||||
rootCoverageComplete &&
|
||||
left.inventory.coverage.scope === "container-entries";
|
||||
const leftCovered =
|
||||
sameScope &&
|
||||
hasCompleteInventoryCoverage(left.inventory, left.inventory.coverage.scope);
|
||||
const rightCovered =
|
||||
sameScope &&
|
||||
hasCompleteInventoryCoverage(
|
||||
right.inventory,
|
||||
right.inventory.coverage.scope,
|
||||
);
|
||||
const fullyCovered =
|
||||
leftCovered &&
|
||||
rightCovered &&
|
||||
(entryCoverageComplete ||
|
||||
(rootCoverageComplete &&
|
||||
left.inventory.manifest.occurrence_count === 1 &&
|
||||
right.inventory.manifest.occurrence_count === 1));
|
||||
const limitations = [
|
||||
...left.inventory.limitations.map((item) => `Left: ${item}`),
|
||||
...right.inventory.limitations.map((item) => `Right: ${item}`),
|
||||
...(leftComplete ? [] : ["Left artifact inventory is incomplete."]),
|
||||
...(rightComplete ? [] : ["Right artifact inventory is incomplete."]),
|
||||
...(sameScope ? [] : ["Artifact inventory scopes differ."]),
|
||||
...(sameScope &&
|
||||
left.inventory.coverage.scope === "root" &&
|
||||
(left.inventory.manifest.occurrence_count > 1 ||
|
||||
right.inventory.manifest.occurrence_count > 1)
|
||||
? ["Container entry coverage is unavailable for child paths."]
|
||||
: []),
|
||||
...(left.inventory.coverage.status === "complete"
|
||||
? []
|
||||
: ["Left artifact inventory traversal coverage is unknown."]),
|
||||
...(right.inventory.coverage.status === "complete"
|
||||
? []
|
||||
: ["Right artifact inventory traversal coverage is unknown."]),
|
||||
];
|
||||
const changes = compareOccurrences(
|
||||
left.inventory,
|
||||
@@ -119,7 +157,7 @@ export const compareArtifacts = (
|
||||
...left.evidence.map(({ evidence_id: id }) => id),
|
||||
...right.evidence.map(({ evidence_id: id }) => id),
|
||||
],
|
||||
leftCovered && rightCovered,
|
||||
{ root: rootCoverageComplete, entries: entryCoverageComplete },
|
||||
);
|
||||
applyRootHashChange(left.inventory, right.inventory, changes, [
|
||||
...left.evidence.map(({ evidence_id: id }) => id),
|
||||
@@ -129,7 +167,7 @@ export const compareArtifacts = (
|
||||
left.inventory,
|
||||
right.inventory,
|
||||
changes,
|
||||
leftCovered && rightCovered,
|
||||
fullyCovered,
|
||||
);
|
||||
return artifactComparisonResultSchema.parse({
|
||||
status:
|
||||
@@ -139,7 +177,7 @@ export const compareArtifacts = (
|
||||
({ classification }) => classification === "contradiction",
|
||||
)
|
||||
? "contradiction"
|
||||
: !leftCovered || !rightCovered
|
||||
: !fullyCovered
|
||||
? "unknown"
|
||||
: changes.some(({ classification }) => classification === "unknown")
|
||||
? "unknown"
|
||||
@@ -158,7 +196,7 @@ const compareOccurrences = (
|
||||
left: InventorySet,
|
||||
right: InventorySet,
|
||||
evidenceLinks: readonly string[],
|
||||
complete: boolean,
|
||||
coverage: { readonly root: boolean; readonly entries: boolean },
|
||||
): ArtifactChange[] => {
|
||||
const leftByPath = new Map(
|
||||
left.occurrences.map((item) => [item.logical_path, item]),
|
||||
@@ -187,7 +225,8 @@ const compareOccurrences = (
|
||||
rightNode: nodeFor(rightOccurrence, rightNodes),
|
||||
leftRelations: leftRelations.get(path) ?? [],
|
||||
rightRelations: rightRelations.get(path) ?? [],
|
||||
complete,
|
||||
rootCovered: coverage.root,
|
||||
entriesCovered: coverage.entries,
|
||||
evidenceLinks,
|
||||
});
|
||||
if (change !== null) output.push(change);
|
||||
@@ -203,7 +242,8 @@ interface ClassifyPathInput {
|
||||
readonly rightNode: ArtifactNode | undefined;
|
||||
readonly leftRelations: readonly RelationProjection[];
|
||||
readonly rightRelations: readonly RelationProjection[];
|
||||
readonly complete: boolean;
|
||||
readonly rootCovered: boolean;
|
||||
readonly entriesCovered: boolean;
|
||||
readonly evidenceLinks: readonly string[];
|
||||
}
|
||||
|
||||
@@ -217,17 +257,18 @@ const buildArtifactChangeBase = (input: ClassifyPathInput) => ({
|
||||
});
|
||||
|
||||
const classifyPresence = (input: ClassifyPathInput): ArtifactChange | null => {
|
||||
const covered = input.path === "." ? input.rootCovered : input.entriesCovered;
|
||||
if (input.leftOccurrence === undefined)
|
||||
return {
|
||||
...buildArtifactChangeBase(input),
|
||||
classification: input.complete ? "added" : "unknown",
|
||||
dimensions: [input.complete ? "content" : "availability"],
|
||||
classification: covered ? "added" : "unknown",
|
||||
dimensions: [covered ? "content" : "availability"],
|
||||
};
|
||||
if (input.rightOccurrence === undefined)
|
||||
return {
|
||||
...buildArtifactChangeBase(input),
|
||||
classification: input.complete ? "removed" : "unknown",
|
||||
dimensions: [input.complete ? "content" : "availability"],
|
||||
classification: covered ? "removed" : "unknown",
|
||||
dimensions: [covered ? "content" : "availability"],
|
||||
};
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -188,8 +188,11 @@ export const artifactGraphManifestSchema = z.object({
|
||||
edge_count: z.number().int().min(0),
|
||||
});
|
||||
|
||||
/** Whether the inventory producer covered its modeled child-entry scope. */
|
||||
export const artifactInventoryCoverageSchema = z.enum(["complete", "partial"]);
|
||||
/** Producer-owned traversal coverage; content certainty remains on each occurrence. */
|
||||
export const artifactInventoryCoverageSchema = z.strictObject({
|
||||
scope: z.enum(["root", "container-entries"]),
|
||||
status: z.enum(["complete", "unknown"]),
|
||||
});
|
||||
export type ArtifactInventoryCoverage = z.infer<
|
||||
typeof artifactInventoryCoverageSchema
|
||||
>;
|
||||
@@ -202,15 +205,19 @@ export const artifactInventoryResultSchema = z.object({
|
||||
edges: z.array(artifactEdgeSchema),
|
||||
provenance: z.array(artifactCommandSchema),
|
||||
integrity_contradictions: z.array(integrityContradictionSchema).default([]),
|
||||
// Omitted by older retained Evidence; the inventory parser owns its compatibility policy.
|
||||
coverage: artifactInventoryCoverageSchema.optional(),
|
||||
limitations: z.array(z.string()),
|
||||
/** Optional only for persisted inventory Evidence written before this field. */
|
||||
coverage_status: artifactInventoryCoverageSchema.optional(),
|
||||
coverage_status: z.enum(["complete", "partial"]).optional(),
|
||||
});
|
||||
|
||||
/** Current inventory producers always classify the coverage they examined. */
|
||||
/** Current producers always declare coverage; historical Evidence inputs may omit it. */
|
||||
export const artifactInventoryOutputSchema =
|
||||
artifactInventoryResultSchema.extend({
|
||||
coverage_status: artifactInventoryCoverageSchema,
|
||||
artifactInventoryResultSchema.safeExtend({
|
||||
coverage: artifactInventoryCoverageSchema,
|
||||
coverage_status:
|
||||
artifactInventoryResultSchema.shape.coverage_status.unwrap(),
|
||||
});
|
||||
export type ArtifactInventoryOutput = z.infer<
|
||||
typeof artifactInventoryOutputSchema
|
||||
|
||||
@@ -18,7 +18,10 @@ import {
|
||||
} from "./artifactIdentity.js";
|
||||
import { parseEvidence, type Evidence } from "./evidence.js";
|
||||
import { artifactInspectionResultSchema } from "./artifactInspection.js";
|
||||
import { ZIP_NON_ENTRY_TAIL_LIMITATION } from "./zipPackageFormat.js";
|
||||
import {
|
||||
isZipPackageFormat,
|
||||
ZIP_NON_ENTRY_TAIL_LIMITATION,
|
||||
} from "./zipPackageFormat.js";
|
||||
|
||||
/** Complete or partially assembled inventory used by comparison workflows. */
|
||||
export interface InventorySet {
|
||||
@@ -30,13 +33,17 @@ export interface InventorySet {
|
||||
readonly limitations: readonly string[];
|
||||
/** True when merged node, occurrence, and edge counts match the manifest. */
|
||||
readonly complete: boolean;
|
||||
readonly coverage_status: ArtifactInventoryCoverage;
|
||||
readonly coverage: ArtifactInventoryCoverage;
|
||||
}
|
||||
|
||||
/** Whether all graph members are present and the producer reports full coverage. */
|
||||
export const hasCompleteInventoryCoverage = (
|
||||
inventory: InventorySet,
|
||||
): boolean => inventory.complete && inventory.coverage_status === "complete";
|
||||
requiredScope: ArtifactInventoryCoverage["scope"],
|
||||
): boolean =>
|
||||
inventory.complete &&
|
||||
inventory.coverage.scope === requiredScope &&
|
||||
inventory.coverage.status === "complete";
|
||||
|
||||
export interface ParsedInventorySet {
|
||||
readonly evidence: readonly Evidence[];
|
||||
@@ -132,11 +139,14 @@ const assembleInventoryResults = (
|
||||
const first = pages[0];
|
||||
if (first === undefined)
|
||||
throw new TypeError("Artifact inventory requires Evidence pages");
|
||||
const coverageStatuses = new Set(pages.map(resolveInventoryCoverageStatus));
|
||||
if (coverageStatuses.size !== 1)
|
||||
throw new TypeError("Artifact inventory pages disagree on coverage status");
|
||||
const coverageStatus = [...coverageStatuses][0]!;
|
||||
const manifestJson = canonicalJson(first.manifest, "Artifact inventory");
|
||||
const coverageJson = canonicalJson(
|
||||
{
|
||||
coverage: first.coverage ?? null,
|
||||
coverage_status: first.coverage_status ?? null,
|
||||
},
|
||||
"Artifact inventory coverage",
|
||||
);
|
||||
const nodes = new Map<string, ArtifactNode>();
|
||||
const occurrences = new Map<string, ArtifactOccurrence>();
|
||||
const paths = new Map<string, ArtifactOccurrence>();
|
||||
@@ -144,6 +154,18 @@ const assembleInventoryResults = (
|
||||
for (const page of pages) {
|
||||
if (canonicalJson(page.manifest, "Artifact inventory") !== manifestJson)
|
||||
throw new TypeError("Artifact inventory pages do not share one manifest");
|
||||
if (
|
||||
canonicalJson(
|
||||
{
|
||||
coverage: page.coverage ?? null,
|
||||
coverage_status: page.coverage_status ?? null,
|
||||
},
|
||||
"Artifact inventory coverage",
|
||||
) !== coverageJson
|
||||
)
|
||||
throw new TypeError(
|
||||
"Artifact inventory pages do not share one coverage declaration",
|
||||
);
|
||||
if (
|
||||
canonicalJson(page.integrity_contradictions, "Artifact inventory") !==
|
||||
canonicalJson(first.integrity_contradictions, "Artifact inventory")
|
||||
@@ -165,12 +187,11 @@ const assembleInventoryResults = (
|
||||
for (const edge of page.edges)
|
||||
mergeExact(edges, edge.edge_id, edge, "edge");
|
||||
}
|
||||
const inventory = orderedInventory(
|
||||
first,
|
||||
pages,
|
||||
{ nodes, occurrences, edges },
|
||||
coverageStatus,
|
||||
);
|
||||
const inventory = orderedInventory(first, pages, {
|
||||
nodes,
|
||||
occurrences,
|
||||
edges,
|
||||
});
|
||||
if (inventory.complete) validateCompleteInventory(inventory);
|
||||
return inventory;
|
||||
};
|
||||
@@ -186,7 +207,6 @@ const orderedInventory = (
|
||||
ArtifactInventoryResult["edges"][number]
|
||||
>;
|
||||
},
|
||||
coverageStatus: ArtifactInventoryCoverage,
|
||||
): InventorySet => {
|
||||
const nodes = [...values.nodes.values()].sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.artifact_id, right.artifact_id),
|
||||
@@ -197,40 +217,78 @@ const orderedInventory = (
|
||||
const edges = [...values.edges.values()].sort((left, right) =>
|
||||
compareUnicodeCodePoints(left.edge_id, right.edge_id),
|
||||
);
|
||||
const limitations = [
|
||||
...new Set(pages.flatMap((inventory) => inventory.limitations)),
|
||||
].sort(compareUnicodeCodePoints);
|
||||
return {
|
||||
manifest: first.manifest,
|
||||
nodes,
|
||||
occurrences,
|
||||
edges,
|
||||
integrityContradictions: first.integrity_contradictions,
|
||||
limitations: [
|
||||
...new Set(pages.flatMap((inventory) => inventory.limitations)),
|
||||
].sort(compareUnicodeCodePoints),
|
||||
limitations,
|
||||
coverage: resolveInventoryCoverage({ ...first, limitations }),
|
||||
complete:
|
||||
nodes.length === first.manifest.node_count &&
|
||||
occurrences.length === first.manifest.occurrence_count &&
|
||||
edges.length === first.manifest.edge_count,
|
||||
coverage_status: coverageStatus,
|
||||
};
|
||||
};
|
||||
|
||||
const resolveInventoryCoverageStatus = (
|
||||
// Normalize both retained coverage formats at this owning boundary.
|
||||
const resolveInventoryCoverage = (
|
||||
inventory: ArtifactInventoryResult,
|
||||
): ArtifactInventoryCoverage => {
|
||||
if (inventory.coverage_status !== undefined) return inventory.coverage_status;
|
||||
// Older retained Evidence has no typed status. Keep only its known
|
||||
// informational notices complete; unknown limitation text fails closed.
|
||||
return inventory.limitations.every(isKnownNonCoverageNotice)
|
||||
? "complete"
|
||||
: "partial";
|
||||
const legacy = legacyInventoryCoverage(
|
||||
inventory.manifest.root_format,
|
||||
inventory.limitations,
|
||||
inventory.integrity_contradictions.length,
|
||||
);
|
||||
if (inventory.coverage !== undefined) {
|
||||
if (
|
||||
inventory.coverage_status !== undefined &&
|
||||
(inventory.coverage.status === "complete") !==
|
||||
(inventory.coverage_status === "complete")
|
||||
)
|
||||
throw new TypeError(
|
||||
"Artifact inventory has conflicting coverage declarations",
|
||||
);
|
||||
return inventory.coverage;
|
||||
}
|
||||
return inventory.coverage_status === undefined
|
||||
? legacy
|
||||
: {
|
||||
...legacy,
|
||||
status:
|
||||
inventory.coverage_status === "complete" ? "complete" : "unknown",
|
||||
};
|
||||
};
|
||||
|
||||
const isKnownNonCoverageNotice = (limitation: string): boolean =>
|
||||
limitation === "Artifact has no child container entries." ||
|
||||
limitation === ZIP_NON_ENTRY_TAIL_LIMITATION ||
|
||||
/^\d+ integrity contradiction\(s\) were recorded; mismatched content is observed-untrusted\.$/u.test(
|
||||
limitation,
|
||||
);
|
||||
// Retained pre-coverage records cannot classify arbitrary prose. Recognize only
|
||||
// the historical producer vocabulary here; unfamiliar diagnostics remain unknown.
|
||||
const legacyInventoryCoverage = (
|
||||
format: ArtifactInventoryResult["manifest"]["root_format"],
|
||||
limitations: readonly string[],
|
||||
contradictionCount: number,
|
||||
): ArtifactInventoryCoverage => ({
|
||||
scope:
|
||||
isZipPackageFormat(format) ||
|
||||
["directory", "asar", "dmg", "pkg", "mach-o-universal"].includes(format)
|
||||
? "container-entries"
|
||||
: "root",
|
||||
status: limitations.some(
|
||||
(limitation) =>
|
||||
limitation !== "Artifact has no child container entries." &&
|
||||
limitation !== ZIP_NON_ENTRY_TAIL_LIMITATION &&
|
||||
!(
|
||||
contradictionCount > 0 &&
|
||||
limitation ===
|
||||
`${String(contradictionCount)} integrity contradiction(s) were recorded; mismatched content is observed-untrusted.`
|
||||
),
|
||||
)
|
||||
? "unknown"
|
||||
: "complete",
|
||||
});
|
||||
|
||||
const validateCompleteInventory = (inventory: InventorySet): void => {
|
||||
const nodeIds = new Set(inventory.nodes.map(({ artifact_id: id }) => id));
|
||||
|
||||
@@ -7,7 +7,9 @@ import type {
|
||||
|
||||
/** Immutable inventory produced by one complete artifact scan. */
|
||||
export interface ArtifactInventorySnapshot {
|
||||
readonly coverage_status: ArtifactInventoryCoverage;
|
||||
readonly coverage_status: NonNullable<
|
||||
ArtifactInventoryResult["coverage_status"]
|
||||
>;
|
||||
readonly manifest: ArtifactInventoryResult["manifest"];
|
||||
readonly nodes: readonly ArtifactNode[];
|
||||
readonly occurrences: ArtifactInventoryResult["occurrences"];
|
||||
@@ -16,5 +18,6 @@ export interface ArtifactInventorySnapshot {
|
||||
ArtifactInventoryResult["provenance"][number]
|
||||
>;
|
||||
readonly integrity_contradictions: readonly IntegrityContradiction[];
|
||||
readonly coverage?: ArtifactInventoryCoverage;
|
||||
readonly limitations: readonly string[];
|
||||
}
|
||||
|
||||
@@ -58,20 +58,67 @@ export const fat = (
|
||||
return bytes;
|
||||
};
|
||||
|
||||
/** Build a bounded PE image with optional managed metadata. */
|
||||
export interface PeCodeViewFixture {
|
||||
readonly type?: "RSDS" | "NB10";
|
||||
readonly guid?: string;
|
||||
readonly age?: number;
|
||||
readonly pdbPath?: string;
|
||||
}
|
||||
|
||||
/** Build a bounded PE image with optional managed metadata or CodeView debug data. */
|
||||
export const pe = (
|
||||
machine: number,
|
||||
offset = 64,
|
||||
characteristics = 0x0002,
|
||||
managed = false,
|
||||
codeView?: PeCodeViewFixture,
|
||||
): Buffer => {
|
||||
const optionalHeaderSize =
|
||||
machine === 0x014c || machine === 0x01c0 ? 224 : 240;
|
||||
const bytes = Buffer.alloc(Math.max(512, offset + 24 + optionalHeaderSize));
|
||||
const sectionTableOffset = offset + 24 + optionalHeaderSize;
|
||||
const sectionCount = codeView !== undefined ? 1 : 0;
|
||||
const rawDataOffset = 512;
|
||||
|
||||
let codeViewData: Buffer | undefined;
|
||||
if (codeView !== undefined) {
|
||||
const type = codeView.type ?? "RSDS";
|
||||
const pdbPath = codeView.pdbPath ?? "app.pdb";
|
||||
const age = codeView.age ?? 1;
|
||||
const pathBuf = Buffer.from(`${pdbPath}\0`, "utf8");
|
||||
if (type === "RSDS") {
|
||||
codeViewData = Buffer.alloc(24 + pathBuf.length);
|
||||
codeViewData.write("RSDS", 0, "ascii");
|
||||
const guid = codeView.guid ?? "01234567-89ab-cdef-0123-456789abcdef";
|
||||
const parts = guid.split("-");
|
||||
if (parts.length === 5) {
|
||||
codeViewData.writeUInt32LE(parseInt(parts[0]!, 16), 4);
|
||||
codeViewData.writeUInt16LE(parseInt(parts[1]!, 16), 8);
|
||||
codeViewData.writeUInt16LE(parseInt(parts[2]!, 16), 10);
|
||||
Buffer.from(parts[3]!, "hex").copy(codeViewData, 12);
|
||||
Buffer.from(parts[4]!, "hex").copy(codeViewData, 14);
|
||||
}
|
||||
codeViewData.writeUInt32LE(age, 20);
|
||||
pathBuf.copy(codeViewData, 24);
|
||||
} else {
|
||||
codeViewData = Buffer.alloc(16 + pathBuf.length);
|
||||
codeViewData.write("NB10", 0, "ascii");
|
||||
codeViewData.writeUInt32LE(0, 4);
|
||||
codeViewData.writeUInt32LE(0, 8);
|
||||
codeViewData.writeUInt32LE(age, 12);
|
||||
pathBuf.copy(codeViewData, 16);
|
||||
}
|
||||
}
|
||||
|
||||
const minSize =
|
||||
codeViewData !== undefined
|
||||
? rawDataOffset + 28 + codeViewData.length + 32
|
||||
: Math.max(512, offset + 24 + optionalHeaderSize);
|
||||
const bytes = Buffer.alloc(minSize);
|
||||
bytes.write("MZ", 0, "ascii");
|
||||
bytes.writeUInt32LE(offset, 0x3c);
|
||||
bytes.write("PE\0\0", offset, "binary");
|
||||
bytes.writeUInt16LE(machine, offset + 4);
|
||||
bytes.writeUInt16LE(sectionCount, offset + 6);
|
||||
bytes.writeUInt16LE(optionalHeaderSize, offset + 20);
|
||||
bytes.writeUInt16LE(characteristics, offset + 22);
|
||||
const optionalHeader = offset + 24;
|
||||
@@ -83,6 +130,29 @@ export const pe = (
|
||||
bytes.writeUInt32LE(0x2000, optionalHeader + directoryOffset + 14 * 8);
|
||||
bytes.writeUInt32LE(72, optionalHeader + directoryOffset + 14 * 8 + 4);
|
||||
}
|
||||
if (codeViewData !== undefined) {
|
||||
const debugDirRva = 0x1000;
|
||||
const debugDirSize = 28;
|
||||
bytes.writeUInt32LE(debugDirRva, optionalHeader + directoryOffset + 6 * 8);
|
||||
bytes.writeUInt32LE(
|
||||
debugDirSize,
|
||||
optionalHeader + directoryOffset + 6 * 8 + 4,
|
||||
);
|
||||
|
||||
bytes.write(".rdata\0\0", sectionTableOffset, "ascii");
|
||||
bytes.writeUInt32LE(0x1000, sectionTableOffset + 8);
|
||||
bytes.writeUInt32LE(0x1000, sectionTableOffset + 12);
|
||||
bytes.writeUInt32LE(512, sectionTableOffset + 16);
|
||||
bytes.writeUInt32LE(rawDataOffset, sectionTableOffset + 20);
|
||||
bytes.writeUInt32LE(0x40000040, sectionTableOffset + 36);
|
||||
|
||||
bytes.writeUInt32LE(2, rawDataOffset + 12);
|
||||
bytes.writeUInt32LE(codeViewData.length, rawDataOffset + 16);
|
||||
bytes.writeUInt32LE(debugDirRva + 28, rawDataOffset + 20);
|
||||
bytes.writeUInt32LE(rawDataOffset + 28, rawDataOffset + 24);
|
||||
|
||||
codeViewData.copy(bytes, rawDataOffset + 28);
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
|
||||
|
||||
+172
-1
@@ -3,6 +3,7 @@ import type {
|
||||
ElfFamilyMetadata,
|
||||
HeaderArchitecture,
|
||||
MipsElfMetadata,
|
||||
PeCodeViewRecord,
|
||||
RiscvElfMetadata,
|
||||
UniversalMachOSlice,
|
||||
} from "./binaryTargetTypes.js";
|
||||
@@ -50,6 +51,7 @@ export type ExecutableMetadata =
|
||||
| "shared-library"
|
||||
| "non-executable";
|
||||
readonly managed: boolean;
|
||||
readonly codeView?: PeCodeViewRecord;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -257,7 +259,8 @@ const parseMz = (
|
||||
);
|
||||
if (windowsHeader.signature !== "PE")
|
||||
return err("invalid or truncated Windows executable header in MZ image");
|
||||
return parsePeRecord(bytes.subarray(offset));
|
||||
const codeView = parsePeCodeViewFromBuffer(bytes, offset);
|
||||
return parsePeRecord(bytes.subarray(offset), codeView);
|
||||
}
|
||||
const parsed = parseDosMzHeader(bytes, fileSize, signatureAtOffset);
|
||||
return parsed.ok
|
||||
@@ -272,6 +275,7 @@ const parseMz = (
|
||||
/** Parse a bounded PE signature, COFF record, and optional header without I/O. */
|
||||
export const parsePeRecord = (
|
||||
record: Buffer,
|
||||
codeView: PeCodeViewRecord | undefined,
|
||||
): Result<ExecutableMetadata, string> => {
|
||||
if (
|
||||
record.length < 24 ||
|
||||
@@ -305,9 +309,176 @@ export const parsePeRecord = (
|
||||
? "application"
|
||||
: "shared-library",
|
||||
managed: managed.value,
|
||||
...(codeView !== undefined ? { codeView } : {}),
|
||||
});
|
||||
};
|
||||
|
||||
/** Parse CodeView record data (RSDS or NB10) from raw data bytes. */
|
||||
export const parseCodeViewData = (
|
||||
buffer: Buffer,
|
||||
sizeOfData: number,
|
||||
): PeCodeViewRecord | undefined => {
|
||||
if (sizeOfData < 16 || buffer.length < 16) return undefined;
|
||||
const magic = buffer.subarray(0, 4).toString("binary");
|
||||
const pathOffset = magic === "RSDS" ? 24 : magic === "NB10" ? 16 : undefined;
|
||||
if (
|
||||
pathOffset === undefined ||
|
||||
sizeOfData <= pathOffset ||
|
||||
buffer.length <= pathOffset
|
||||
)
|
||||
return undefined;
|
||||
const end = buffer.indexOf(0, pathOffset);
|
||||
if (end <= pathOffset || end >= sizeOfData) return undefined;
|
||||
const pdbPath = buffer.toString("utf8", pathOffset, end);
|
||||
const pdbFileName = pdbPath.split(/[/\\]/u).pop() ?? pdbPath;
|
||||
if (magic === "RSDS" && sizeOfData >= 24 && buffer.length >= 24) {
|
||||
const part1 = buffer.readUInt32LE(4).toString(16).padStart(8, "0");
|
||||
const part2 = buffer.readUInt16LE(8).toString(16).padStart(4, "0");
|
||||
const part3 = buffer.readUInt16LE(10).toString(16).padStart(4, "0");
|
||||
const part4 = buffer.subarray(12, 14).toString("hex");
|
||||
const part5 = buffer.subarray(14, 20).toString("hex");
|
||||
const guid = `${part1}-${part2}-${part3}-${part4}-${part5}`;
|
||||
const age = buffer.readUInt32LE(20);
|
||||
return { pdbPath, pdbFileName, guid, age };
|
||||
}
|
||||
if (magic === "NB10") {
|
||||
const age = buffer.readUInt32LE(12);
|
||||
return { pdbPath, pdbFileName, guid: null, age };
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Locate the debug directory RVA and size from a PE optional header. */
|
||||
export const peDebugDirectoryEntry = (
|
||||
optionalHeader: Buffer,
|
||||
):
|
||||
| {
|
||||
readonly debugRva: number;
|
||||
readonly debugSize: number;
|
||||
readonly sizeOfHeaders: number;
|
||||
}
|
||||
| undefined => {
|
||||
if (optionalHeader.length < 2) return undefined;
|
||||
const magic = optionalHeader.readUInt16LE(0);
|
||||
if (magic !== 0x10b && magic !== 0x20b) return undefined;
|
||||
const directoryOffset = magic === 0x20b ? 112 : 96;
|
||||
const directoryCountOffset = directoryOffset - 4;
|
||||
if (optionalHeader.length < directoryCountOffset + 4) return undefined;
|
||||
const directoryCount = optionalHeader.readUInt32LE(directoryCountOffset);
|
||||
if (directoryCount <= 6) return undefined;
|
||||
const debugEntryOffset = directoryOffset + 6 * 8;
|
||||
if (optionalHeader.length < debugEntryOffset + 8) return undefined;
|
||||
const debugRva = optionalHeader.readUInt32LE(debugEntryOffset);
|
||||
const debugSize = optionalHeader.readUInt32LE(debugEntryOffset + 4);
|
||||
if (debugRva === 0 || debugSize < 28) return undefined;
|
||||
return {
|
||||
debugRva,
|
||||
debugSize,
|
||||
sizeOfHeaders: optionalHeader.readUInt32LE(60),
|
||||
};
|
||||
};
|
||||
|
||||
/** Translate an RVA to an absolute file offset using the PE section table. */
|
||||
export const peRvaToFileOffset = (
|
||||
rva: number,
|
||||
sectionTable: Buffer,
|
||||
numberOfSections: number,
|
||||
sizeOfHeaders: number,
|
||||
size = 1,
|
||||
): number | undefined => {
|
||||
if (rva < sizeOfHeaders && size <= sizeOfHeaders - rva) return rva;
|
||||
for (let i = 0; i < numberOfSections; i++) {
|
||||
const offset = i * 40;
|
||||
if (sectionTable.length < offset + 40) break;
|
||||
const virtualAddress = sectionTable.readUInt32LE(offset + 12);
|
||||
const sizeOfRawData = sectionTable.readUInt32LE(offset + 16);
|
||||
const pointerToRawData = sectionTable.readUInt32LE(offset + 20);
|
||||
if (
|
||||
pointerToRawData > 0 &&
|
||||
rva >= virtualAddress &&
|
||||
rva - virtualAddress < sizeOfRawData &&
|
||||
size <= sizeOfRawData - (rva - virtualAddress)
|
||||
) {
|
||||
return pointerToRawData + (rva - virtualAddress);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Parse CodeView metadata from a bounded in-memory PE image buffer. */
|
||||
export const parsePeCodeViewFromBuffer = (
|
||||
fileBytes: Buffer,
|
||||
peOffset: number,
|
||||
): PeCodeViewRecord | undefined => {
|
||||
if (fileBytes.length < peOffset + 24) return undefined;
|
||||
const numberOfSections = fileBytes.readUInt16LE(peOffset + 6);
|
||||
const optionalHeaderSize = fileBytes.readUInt16LE(peOffset + 20);
|
||||
if (
|
||||
optionalHeaderSize > 4096 ||
|
||||
fileBytes.length < peOffset + 24 + optionalHeaderSize
|
||||
)
|
||||
return undefined;
|
||||
const optionalHeader = fileBytes.subarray(
|
||||
peOffset + 24,
|
||||
peOffset + 24 + optionalHeaderSize,
|
||||
);
|
||||
const debugDir = peDebugDirectoryEntry(optionalHeader);
|
||||
if (debugDir === undefined) return undefined;
|
||||
const sectionTableOffset = peOffset + 24 + optionalHeaderSize;
|
||||
const sectionTableSize = numberOfSections * 40;
|
||||
if (fileBytes.length < sectionTableOffset + sectionTableSize)
|
||||
return undefined;
|
||||
const sectionTable = fileBytes.subarray(
|
||||
sectionTableOffset,
|
||||
sectionTableOffset + sectionTableSize,
|
||||
);
|
||||
const debugFileOffset = peRvaToFileOffset(
|
||||
debugDir.debugRva,
|
||||
sectionTable,
|
||||
numberOfSections,
|
||||
debugDir.sizeOfHeaders,
|
||||
Math.min(debugDir.debugSize, 4096),
|
||||
);
|
||||
if (debugFileOffset === undefined || fileBytes.length < debugFileOffset + 28)
|
||||
return undefined;
|
||||
const maxDebugBytes = Math.min(
|
||||
debugDir.debugSize,
|
||||
fileBytes.length - debugFileOffset,
|
||||
);
|
||||
const numEntries = Math.floor(maxDebugBytes / 28);
|
||||
for (let j = 0; j < numEntries; j++) {
|
||||
const entryOffset = debugFileOffset + j * 28;
|
||||
const type = fileBytes.readUInt32LE(entryOffset + 12);
|
||||
if (type !== 2) continue; // IMAGE_DEBUG_TYPE_CODEVIEW
|
||||
const sizeOfData = fileBytes.readUInt32LE(entryOffset + 16);
|
||||
let pointerToRawData = fileBytes.readUInt32LE(entryOffset + 24);
|
||||
if (pointerToRawData === 0) {
|
||||
const addressOfRawData = fileBytes.readUInt32LE(entryOffset + 20);
|
||||
const mapped = peRvaToFileOffset(
|
||||
addressOfRawData,
|
||||
sectionTable,
|
||||
numberOfSections,
|
||||
debugDir.sizeOfHeaders,
|
||||
Math.min(sizeOfData, 4096),
|
||||
);
|
||||
if (mapped === undefined) continue;
|
||||
pointerToRawData = mapped;
|
||||
}
|
||||
if (
|
||||
sizeOfData < 16 ||
|
||||
fileBytes.length < pointerToRawData + Math.min(sizeOfData, 24)
|
||||
)
|
||||
continue;
|
||||
const codeViewData = fileBytes.subarray(
|
||||
pointerToRawData,
|
||||
Math.min(fileBytes.length, pointerToRawData + sizeOfData),
|
||||
);
|
||||
const parsed = parseCodeViewData(codeViewData, sizeOfData);
|
||||
if (parsed !== undefined) return parsed;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const peManagedStatus = (
|
||||
optionalHeader: Buffer,
|
||||
architecture: BinaryArchitecture,
|
||||
|
||||
@@ -24,6 +24,14 @@ export interface ElfFamilyMetadata {
|
||||
readonly flags: number;
|
||||
}
|
||||
|
||||
/** Referenced CodeView debug symbols from a PE debug directory. */
|
||||
export interface PeCodeViewRecord {
|
||||
readonly pdbPath: string;
|
||||
readonly pdbFileName: string;
|
||||
readonly guid: string | null;
|
||||
readonly age: number;
|
||||
}
|
||||
|
||||
/** Families whose parsed target variant carries `ElfFamilyMetadata`. */
|
||||
export const ELF_METADATA_ARCHITECTURES = [
|
||||
"mips",
|
||||
@@ -170,6 +178,8 @@ export type BinaryTarget =
|
||||
| "non-executable";
|
||||
/** Whether the PE declares a non-empty CLI header data-directory entry. */
|
||||
readonly managed: boolean;
|
||||
/** Referenced CodeView debug symbols from the PE debug directory, if present. */
|
||||
readonly codeView?: PeCodeViewRecord;
|
||||
})
|
||||
| (BinaryTargetIdentity &
|
||||
NonExecutableMetadata & {
|
||||
|
||||
@@ -72,7 +72,7 @@ export const harmonyApplicationProjectionResultSchema = z.strictObject({
|
||||
// Count completeness is separate from the declared coverage status.
|
||||
inventory_complete: z.boolean(),
|
||||
}),
|
||||
limitations: z.array(z.string().min(1)),
|
||||
limitations: z.array(z.string()),
|
||||
});
|
||||
|
||||
export type HarmonyApplicationProjectionInput = z.infer<
|
||||
@@ -112,7 +112,8 @@ export const projectHarmonyApplication = (
|
||||
classified.native_libraries,
|
||||
);
|
||||
const limitations = [
|
||||
...(!hasCompleteInventoryCoverage(inventory)
|
||||
...inventory.limitations,
|
||||
...(!hasCompleteInventoryCoverage(inventory, "container-entries")
|
||||
? ["Source inventory coverage is partial; absence is unknown."]
|
||||
: []),
|
||||
...(unavailable.length > 0
|
||||
|
||||
@@ -77,15 +77,23 @@ export const matchJavaScriptApplicationVersions = (
|
||||
): ApplicationVersionMatchingProjection => {
|
||||
const left = new Map(leftNodes.map((node) => [node.node_id, node]));
|
||||
const right = new Map(rightNodes.map((node) => [node.node_id, node]));
|
||||
const leftCandidates = new Map<string, Set<string>>();
|
||||
const rightCandidates = new Map<string, Set<string>>();
|
||||
const pairs: ApplicationVersionNodePair[] = [];
|
||||
const ambiguousGroups: AmbiguousGroup[] = [];
|
||||
for (const tier of matchTiers())
|
||||
applyTier({ left, right, pairs, leftCandidates, rightCandidates, tier });
|
||||
applyTier({ left, right, pairs, ambiguousGroups, tier });
|
||||
const remainingLeft = sortedNodes(left.values());
|
||||
const remainingRight = sortedNodes(right.values());
|
||||
const leftIds = new Set(remainingLeft.map(({ node_id: id }) => id));
|
||||
const rightIds = new Set(remainingRight.map(({ node_id: id }) => id));
|
||||
const leftCandidates = new Map<string, Set<string>>();
|
||||
const rightCandidates = new Map<string, Set<string>>();
|
||||
// Later tiers can pair members of an ambiguous group. Expand only survivors,
|
||||
// without pairing groups that became unique after those later matches.
|
||||
for (const group of ambiguousGroups) {
|
||||
const leftGroup = group.left.filter((node) => left.has(node.node_id));
|
||||
if (leftGroup.length === 0) continue;
|
||||
const rightGroup = group.right.filter((node) => right.has(node.node_id));
|
||||
if (rightGroup.length === 0) continue;
|
||||
recordCandidates(leftGroup, rightGroup, leftCandidates, rightCandidates);
|
||||
}
|
||||
return {
|
||||
pairs: pairs.sort((a, b) =>
|
||||
compareUnicodeCodePoints(
|
||||
@@ -95,17 +103,21 @@ export const matchJavaScriptApplicationVersions = (
|
||||
),
|
||||
unmatchedLeft: remainingLeft,
|
||||
unmatchedRight: remainingRight,
|
||||
candidatesForLeft: filteredCandidates(leftCandidates, rightIds),
|
||||
candidatesForRight: filteredCandidates(rightCandidates, leftIds),
|
||||
candidatesForLeft: sortedCandidates(leftCandidates),
|
||||
candidatesForRight: sortedCandidates(rightCandidates),
|
||||
};
|
||||
};
|
||||
|
||||
interface AmbiguousGroup {
|
||||
readonly left: readonly ApplicationNode[];
|
||||
readonly right: readonly ApplicationNode[];
|
||||
}
|
||||
|
||||
interface TierContext {
|
||||
readonly left: Map<string, ApplicationNode>;
|
||||
readonly right: Map<string, ApplicationNode>;
|
||||
readonly pairs: ApplicationVersionNodePair[];
|
||||
readonly leftCandidates: Map<string, Set<string>>;
|
||||
readonly rightCandidates: Map<string, Set<string>>;
|
||||
readonly ambiguousGroups: AmbiguousGroup[];
|
||||
readonly tier: MatchTier;
|
||||
}
|
||||
|
||||
@@ -114,15 +126,12 @@ const applyTier = (context: TierContext): void => {
|
||||
const rightByKey = nodesByKey(context.right.values(), context.tier.key);
|
||||
for (const key of [...leftByKey.keys()].sort(compareUnicodeCodePoints)) {
|
||||
const leftGroup = leftByKey.get(key) ?? [];
|
||||
const rightGroup = rightByKey.get(key) ?? [];
|
||||
if (rightGroup.length === 0) continue;
|
||||
recordCandidates(
|
||||
leftGroup,
|
||||
rightGroup,
|
||||
context.leftCandidates,
|
||||
context.rightCandidates,
|
||||
);
|
||||
if (leftGroup.length !== 1 || rightGroup.length !== 1) continue;
|
||||
const rightGroup = rightByKey.get(key);
|
||||
if (rightGroup === undefined) continue;
|
||||
if (leftGroup.length !== 1 || rightGroup.length !== 1) {
|
||||
context.ambiguousGroups.push({ left: leftGroup, right: rightGroup });
|
||||
continue;
|
||||
}
|
||||
const leftNode = leftGroup[0];
|
||||
const rightNode = rightGroup[0];
|
||||
if (leftNode === undefined || rightNode === undefined) continue;
|
||||
@@ -144,7 +153,9 @@ const nodesByKey = (
|
||||
for (const node of nodes) {
|
||||
const key = keyFor(node);
|
||||
if (key === null) continue;
|
||||
output.set(key, [...(output.get(key) ?? []), node]);
|
||||
const group = output.get(key);
|
||||
if (group === undefined) output.set(key, [node]);
|
||||
else group.push(node);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
@@ -169,17 +180,14 @@ const recordCandidates = (
|
||||
}
|
||||
};
|
||||
|
||||
const filteredCandidates = (
|
||||
const sortedCandidates = (
|
||||
candidates: ReadonlyMap<string, ReadonlySet<string>>,
|
||||
retained: ReadonlySet<string>,
|
||||
): ReadonlyMap<string, readonly string[]> =>
|
||||
new Map(
|
||||
[...candidates.entries()].flatMap(([nodeId, values]) => {
|
||||
const filtered = [...values]
|
||||
.filter((value) => retained.has(value))
|
||||
.sort(compareUnicodeCodePoints);
|
||||
return filtered.length === 0 ? [] : [[nodeId, filtered] as const];
|
||||
}),
|
||||
[...candidates.entries()].map(([nodeId, values]) => [
|
||||
nodeId,
|
||||
[...values].sort(compareUnicodeCodePoints),
|
||||
]),
|
||||
);
|
||||
|
||||
const nodeIdentityKey = (node: ApplicationNode): string =>
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
artifactEvidence,
|
||||
completeCoverage,
|
||||
fixtureNode,
|
||||
} from "./javascriptApplicationGraph.fixture.js";
|
||||
import {
|
||||
createJavaScriptApplicationGraph,
|
||||
type JavaScriptApplicationGraph,
|
||||
} from "./javascriptApplicationGraph.js";
|
||||
import type { ApplicationNode } from "./javascriptApplicationGraphSchemas.js";
|
||||
import { compareJavaScriptApplicationVersions } from "./javascriptApplicationVersionComparison.js";
|
||||
|
||||
const ARTIFACT_SHA = "a".repeat(64);
|
||||
|
||||
const moduleNode = (name: string, source: string, structure?: string) =>
|
||||
fixtureNode(
|
||||
"javascript-module",
|
||||
{
|
||||
strategy: "canonical-path",
|
||||
stability: "artifact-version",
|
||||
artifact_sha256: ARTIFACT_SHA,
|
||||
path: `${name}.js`,
|
||||
},
|
||||
name,
|
||||
{
|
||||
source_sha256: source.repeat(64),
|
||||
...(structure === undefined
|
||||
? {}
|
||||
: {
|
||||
structural_fingerprint_sha256: structure.repeat(64),
|
||||
structural_fingerprint_algorithm: "fixture",
|
||||
}),
|
||||
},
|
||||
artifactEvidence(ARTIFACT_SHA, `${name}.js`),
|
||||
);
|
||||
|
||||
const graph = (nodes: ApplicationNode[]): JavaScriptApplicationGraph =>
|
||||
createJavaScriptApplicationGraph({
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids: nodes.map(({ node_id }) => node_id),
|
||||
nodes,
|
||||
edges: [],
|
||||
coverage: completeCoverage,
|
||||
limitations: [],
|
||||
});
|
||||
|
||||
const compare = (
|
||||
left: JavaScriptApplicationGraph,
|
||||
right: JavaScriptApplicationGraph,
|
||||
) =>
|
||||
compareJavaScriptApplicationVersions({
|
||||
left: {
|
||||
evidenceId: `ev_${"1".repeat(64)}`,
|
||||
rootArtifactSha256: ARTIFACT_SHA,
|
||||
graph: left,
|
||||
},
|
||||
right: {
|
||||
evidenceId: `ev_${"2".repeat(64)}`,
|
||||
rootArtifactSha256: ARTIFACT_SHA,
|
||||
graph: right,
|
||||
},
|
||||
leftNativeEvidence: [],
|
||||
rightNativeEvidence: [],
|
||||
});
|
||||
|
||||
describe("application version matching", () => {
|
||||
it("does not revisit a source group that becomes unique after identity matching", () => {
|
||||
const shared = moduleNode("shared", "1");
|
||||
const leftOnly = moduleNode("left", "1");
|
||||
const rightOnly = moduleNode("right", "1");
|
||||
const result = compare(
|
||||
graph([shared, leftOnly]),
|
||||
graph([shared, rightOnly]),
|
||||
);
|
||||
|
||||
expect(result.matching).toMatchObject({
|
||||
exact_node_identity: 1,
|
||||
exact_module_source_digest: 0,
|
||||
ambiguous: 2,
|
||||
unmatched: 0,
|
||||
});
|
||||
expect(
|
||||
result.items.find(({ left_node_id }) => left_node_id === shared.node_id),
|
||||
).toMatchObject({
|
||||
right_node_id: shared.node_id,
|
||||
match: { status: "matched", basis: "exact-node-identity" },
|
||||
});
|
||||
expect(
|
||||
result.items.find(
|
||||
({ left_node_id }) => left_node_id === leftOnly.node_id,
|
||||
),
|
||||
).toMatchObject({
|
||||
status: "unknown",
|
||||
right_node_id: null,
|
||||
match: {
|
||||
status: "ambiguous",
|
||||
candidate_right_node_ids: [rightOnly.node_id],
|
||||
},
|
||||
});
|
||||
expect(
|
||||
result.items.find(
|
||||
({ right_node_id }) => right_node_id === rightOnly.node_id,
|
||||
),
|
||||
).toMatchObject({
|
||||
status: "unknown",
|
||||
left_node_id: null,
|
||||
match: {
|
||||
status: "ambiguous",
|
||||
candidate_left_node_ids: [leftOnly.node_id],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("unions all unresolved source and structural candidates without forcing pairs", () => {
|
||||
const a = moduleNode("a", "1", "a");
|
||||
const b = moduleNode("b", "1", "a");
|
||||
const c = moduleNode("c", "2", "a");
|
||||
const x = moduleNode("x", "1", "a");
|
||||
const y = moduleNode("y", "1", "b");
|
||||
const z = moduleNode("z", "3", "a");
|
||||
const left = graph([a, b, c]);
|
||||
const right = graph([x, y, z]);
|
||||
const result = compare(left, right);
|
||||
|
||||
expect(result.summary).toEqual({
|
||||
unchanged: 0,
|
||||
added: 0,
|
||||
removed: 0,
|
||||
changed: 0,
|
||||
unknown: 6,
|
||||
});
|
||||
expect(result.matching).toMatchObject({ ambiguous: 6, unmatched: 0 });
|
||||
for (const [node, candidates] of [
|
||||
[a, [x, y, z]],
|
||||
[b, [x, y, z]],
|
||||
[c, [x, z]],
|
||||
] as const) {
|
||||
expect(
|
||||
result.items.find(({ left_node_id }) => left_node_id === node.node_id)
|
||||
?.match,
|
||||
).toEqual({
|
||||
status: "ambiguous",
|
||||
basis: "none",
|
||||
confidence: "unknown",
|
||||
candidate_left_node_ids: [],
|
||||
candidate_right_node_ids: candidates
|
||||
.map(({ node_id }) => node_id)
|
||||
.sort(),
|
||||
});
|
||||
}
|
||||
for (const [node, candidates] of [
|
||||
[x, [a, b, c]],
|
||||
[y, [a, b]],
|
||||
[z, [a, b, c]],
|
||||
] as const) {
|
||||
expect(
|
||||
result.items.find(({ right_node_id }) => right_node_id === node.node_id)
|
||||
?.match,
|
||||
).toEqual({
|
||||
status: "ambiguous",
|
||||
basis: "none",
|
||||
confidence: "unknown",
|
||||
candidate_left_node_ids: candidates
|
||||
.map(({ node_id }) => node_id)
|
||||
.sort(),
|
||||
candidate_right_node_ids: [],
|
||||
});
|
||||
}
|
||||
|
||||
// Permute the already validated graph so matching sees the changed order.
|
||||
// Compare the full result, including item, comparison and change-graph IDs.
|
||||
expect(
|
||||
compare(
|
||||
{ ...left, nodes: [...left.nodes].reverse() },
|
||||
{ ...right, nodes: [...right.nodes].reverse() },
|
||||
),
|
||||
).toEqual(result);
|
||||
});
|
||||
});
|
||||
@@ -130,3 +130,74 @@ describe("JavaScript semantic analysis: runtime boundaries 1", () => {
|
||||
expect(ir.objectOperations.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript semantic analysis: event dispatch data", () => {
|
||||
it("keeps emitted data out of dispatch listener fields", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
import { EventEmitter } from "node:events";
|
||||
const bus = new EventEmitter();
|
||||
const data = 1;
|
||||
const handler = () => {};
|
||||
bus.on("ready", handler);
|
||||
bus.emit("ready", data);
|
||||
bus.emit("ready");
|
||||
bus.off("ready", handler);
|
||||
bus.removeAllListeners("ready", data);
|
||||
`);
|
||||
const handler = ir.bindings.find(({ name }) => name === "handler");
|
||||
expect(handler).toBeDefined();
|
||||
expect(
|
||||
ir.eventOperations.map(
|
||||
({ kind, method, listenerBindingId, listenerLocation }) => ({
|
||||
kind,
|
||||
method,
|
||||
listenerBindingId,
|
||||
hasListenerLocation: listenerLocation !== null,
|
||||
}),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
kind: "register",
|
||||
method: "on",
|
||||
listenerBindingId: handler?.bindingId,
|
||||
hasListenerLocation: true,
|
||||
},
|
||||
{
|
||||
kind: "dispatch",
|
||||
method: "emit",
|
||||
listenerBindingId: null,
|
||||
hasListenerLocation: false,
|
||||
},
|
||||
{
|
||||
kind: "dispatch",
|
||||
method: "emit",
|
||||
listenerBindingId: null,
|
||||
hasListenerLocation: false,
|
||||
},
|
||||
{
|
||||
kind: "remove",
|
||||
method: "off",
|
||||
listenerBindingId: handler?.bindingId,
|
||||
hasListenerLocation: true,
|
||||
},
|
||||
{
|
||||
kind: "remove",
|
||||
method: "removeAllListeners",
|
||||
listenerBindingId: null,
|
||||
hasListenerLocation: false,
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
ir.eventOperations.map(({ eventName, resolution }) => [
|
||||
eventName,
|
||||
resolution,
|
||||
]),
|
||||
).toEqual([
|
||||
["ready", "complete"],
|
||||
["ready", "complete"],
|
||||
["ready", "complete"],
|
||||
["ready", "complete"],
|
||||
["ready", "complete"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,6 +37,13 @@ const EVENT_REMOVE_METHODS = [
|
||||
"removeListener",
|
||||
"removeAllListeners",
|
||||
] as const;
|
||||
/**
|
||||
* Methods whose argument position 1 names a listener. `removeAllListeners`
|
||||
* takes only an optional event name, and `emit` passes its remaining
|
||||
* arguments as data to listeners that already exist.
|
||||
*/
|
||||
const EVENT_LISTENER_METHODS: readonly JavaScriptSemanticEventOperation["method"][] =
|
||||
[...EVENT_REGISTER_METHODS, "off", "removeListener"];
|
||||
const EVENT_METHODS = [
|
||||
...EVENT_REGISTER_METHODS,
|
||||
...EVENT_REMOVE_METHODS,
|
||||
@@ -137,7 +144,9 @@ const collectEvent = (
|
||||
if (method === undefined) return;
|
||||
const kind = eventKind(method);
|
||||
const eventArgument = node.arguments[0];
|
||||
const listenerArgument = node.arguments[1];
|
||||
const listenerArgument = EVENT_LISTENER_METHODS.includes(method)
|
||||
? node.arguments[1]
|
||||
: undefined;
|
||||
const emitter = emitterIdentity(member.object, state);
|
||||
const eventName = dataEffectLiteralString(eventArgument);
|
||||
output.push({
|
||||
|
||||
@@ -219,6 +219,16 @@ const managedMemberReferenceSchema = z.object({
|
||||
signature: managedSignatureSchema,
|
||||
});
|
||||
|
||||
const managedUserStringSchema = z.object({
|
||||
token: tokenSchema,
|
||||
offset: offsetSchema.describe("File offset of the #US item"),
|
||||
value: z
|
||||
.string()
|
||||
.describe(
|
||||
"Decoded UTF-16 code units, excluding the terminal flag byte; unpaired surrogates are kept",
|
||||
),
|
||||
});
|
||||
|
||||
const managedModuleReferenceSchema = z.object({
|
||||
token: tokenSchema,
|
||||
row_offset: offsetSchema,
|
||||
@@ -457,6 +467,17 @@ export const managedMemberInspectionSchema = z.object({
|
||||
member_refs: z.array(managedMemberReferenceSchema),
|
||||
call_edges: z.array(managedCallEdgeSchema),
|
||||
field_accesses: z.array(managedFieldAccessSchema),
|
||||
// Distinct #US literals loaded by ldstr in the inspected method bodies, in
|
||||
// token order, with their own coverage: #US issues never enter the member
|
||||
// coverage. null means they were not examined, not that there were none.
|
||||
user_strings: z
|
||||
.object({
|
||||
state: z.enum(["complete", "partial"]),
|
||||
literals: z.array(managedUserStringSchema),
|
||||
issues: z.array(managedParseIssueSchema),
|
||||
})
|
||||
.nullable()
|
||||
.default(null),
|
||||
/** Caller-selected declaring types; null when every type was inspected. */
|
||||
selection: z
|
||||
.object({
|
||||
|
||||
@@ -131,5 +131,8 @@ export const comparisonLimitations = (
|
||||
limitations.push(
|
||||
"ExportedType rows, including type forwarders, are observed but not compared; an unchanged comparison does not establish an unchanged exported or forwarded type set.",
|
||||
);
|
||||
limitations.push(
|
||||
"ldstr literals are not compared; normalized CIL keeps #US tokens rather than decoded text, so an unchanged method does not establish unchanged string literals.",
|
||||
);
|
||||
return limitations;
|
||||
};
|
||||
|
||||
@@ -73,7 +73,11 @@ const methodDimensions = (
|
||||
if (bothAbsent) return { dimensions, bodyUnknown: false };
|
||||
if (!leftBodyComplete || !rightBodyComplete)
|
||||
return { dimensions, bodyUnknown: true };
|
||||
if (left.body.normalized_il_sha256 !== right.body.normalized_il_sha256)
|
||||
// The decoded fingerprint omits switch targets and is not exact CIL identity.
|
||||
if (
|
||||
left.body.il_sha256 !== right.body.il_sha256 ||
|
||||
left.body.normalized_il_sha256 !== right.body.normalized_il_sha256
|
||||
)
|
||||
dimensions.push("cil");
|
||||
if (
|
||||
canonicalize(left.body.opcode_counts) !==
|
||||
|
||||
@@ -49,10 +49,13 @@ interface Ambiguous<Item> {
|
||||
const keyMethod = (item: Method): Keyed<Method> => ({
|
||||
item,
|
||||
exactKey:
|
||||
item.body.status === "present" && item.body.normalized_il_sha256 !== null
|
||||
item.body.status === "present" &&
|
||||
item.body.il_sha256 !== null &&
|
||||
item.body.normalized_il_sha256 !== null
|
||||
? sha256([
|
||||
"method-exact",
|
||||
item.signature.raw_sha256,
|
||||
item.body.il_sha256,
|
||||
item.body.normalized_il_sha256,
|
||||
])
|
||||
: null,
|
||||
|
||||
@@ -56,7 +56,8 @@ export const projectionCoverage = (
|
||||
inventory_complete: boolean;
|
||||
} => ({
|
||||
status:
|
||||
hasCompleteInventoryCoverage(inventory) && budgetComplete
|
||||
hasCompleteInventoryCoverage(inventory, "container-entries") &&
|
||||
budgetComplete
|
||||
? ("complete-within-inventory" as const)
|
||||
: ("partial" as const),
|
||||
inventory_complete: inventory.complete,
|
||||
|
||||
@@ -44,6 +44,16 @@ const dossier = jsonValueSchema.parse(
|
||||
referenced_names: [],
|
||||
native_api: null,
|
||||
native_value_flow: null,
|
||||
facet_availability: {
|
||||
pseudocode: { available: true },
|
||||
assembly: { available: true },
|
||||
comments: { available: true },
|
||||
callers: { available: true },
|
||||
callees: { available: true },
|
||||
references: { available: true },
|
||||
strings_names: { available: true },
|
||||
cfg: { available: true },
|
||||
},
|
||||
basic_blocks: [{ start: "0x1000", end: "0x1001", successors: [] }],
|
||||
limitations: [],
|
||||
}),
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import { z } from "zod";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { localPathStringSchema } from "../localPath.js";
|
||||
|
||||
export const RUST_AUDITABLE_FORMAT_VERSION = 1;
|
||||
|
||||
const index = z.number().int().nonnegative();
|
||||
const rustcHash = z.string().regex(/^[0-9a-f]{40}$/u);
|
||||
|
||||
/** One embedded package, with omitted kind and root preserved as null and false. */
|
||||
export const rustPackageSchema = z.strictObject({
|
||||
name: z.string().min(1),
|
||||
version: z.string().min(1),
|
||||
source: z.string().min(1),
|
||||
kind: z.enum(["runtime", "build"]).nullable(),
|
||||
dependencies: z.array(index),
|
||||
root: z.boolean(),
|
||||
});
|
||||
|
||||
const dependencyEdgeSchema = z.strictObject({
|
||||
from_index: index,
|
||||
to_index: index,
|
||||
});
|
||||
|
||||
/** zlib JSON from one `.dep-v0` section, in the section's package order. */
|
||||
export const rustAuditableMetadataSchema = z.strictObject({
|
||||
section_name: z.enum([".dep-v0", "__DATA,.dep-v0"]),
|
||||
file_offset: index,
|
||||
section_bytes: z.number().int().positive(),
|
||||
compressed_bytes: z.number().int().positive(),
|
||||
uncompressed_bytes: z.number().int().positive(),
|
||||
format_version: z.literal(RUST_AUDITABLE_FORMAT_VERSION).nullable(),
|
||||
packages: z.array(rustPackageSchema),
|
||||
root_package_index: index.nullable(),
|
||||
dependency_edges: z.array(dependencyEdgeSchema),
|
||||
cyclic_dependency_indices: z.array(index),
|
||||
});
|
||||
|
||||
/** Inferred compiler id from read-only `/rustc/<40-hex>/` paths. */
|
||||
export const rustcCommitSchema = z.strictObject({
|
||||
evidence_kind: z.literal("inferred"),
|
||||
hashes: z
|
||||
.array(
|
||||
z.strictObject({
|
||||
hash: rustcHash,
|
||||
file_offset: index,
|
||||
}),
|
||||
)
|
||||
.min(1),
|
||||
});
|
||||
|
||||
/** One explicit local executable or library, independent of a disassembler target. */
|
||||
export const inspectRustBinaryInputSchema = z.strictObject({
|
||||
path: localPathStringSchema.describe(
|
||||
"Absolute filesystem path to the selected executable or library",
|
||||
),
|
||||
});
|
||||
|
||||
/** Bounded cargo-auditable observation. A missing section is null, not an empty dependency list. */
|
||||
export const rustBinarySchema = z
|
||||
.strictObject({
|
||||
artifact: z.strictObject({
|
||||
path: localPathStringSchema,
|
||||
sha256: digestSchema,
|
||||
bytes: index,
|
||||
}),
|
||||
format: z.enum(["elf", "pe", "macho"]),
|
||||
architecture: z.string(),
|
||||
bits: z.union([z.literal(32), z.literal(64)]),
|
||||
byte_order: z.enum(["little", "big"]),
|
||||
auditable_metadata: rustAuditableMetadataSchema.nullable(),
|
||||
rustc_commit: rustcCommitSchema.nullable(),
|
||||
limitations: z.array(z.string()),
|
||||
})
|
||||
.superRefine((report, context) => {
|
||||
const metadata = report.auditable_metadata;
|
||||
if (metadata !== null) {
|
||||
checkSection(report.artifact.bytes, metadata, context);
|
||||
checkPackages(metadata, context);
|
||||
}
|
||||
checkRustc(report.artifact.bytes, report.rustc_commit, context);
|
||||
});
|
||||
|
||||
export type RustPackage = z.infer<typeof rustPackageSchema>;
|
||||
export type RustAuditableMetadata = z.infer<typeof rustAuditableMetadataSchema>;
|
||||
export type RustBinary = z.infer<typeof rustBinarySchema>;
|
||||
export type InspectRustBinaryInput = z.infer<
|
||||
typeof inspectRustBinaryInputSchema
|
||||
>;
|
||||
|
||||
const issue = (
|
||||
context: z.RefinementCtx,
|
||||
path: (string | number)[],
|
||||
message: string,
|
||||
): void => {
|
||||
context.addIssue({ code: "custom", path, message });
|
||||
};
|
||||
|
||||
const inside = (fileBytes: number, offset: number, size: number): boolean =>
|
||||
offset <= fileBytes && size <= fileBytes - offset;
|
||||
|
||||
const checkSection = (
|
||||
fileBytes: number,
|
||||
metadata: RustAuditableMetadata,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
if (
|
||||
!inside(fileBytes, metadata.file_offset, metadata.section_bytes) ||
|
||||
metadata.compressed_bytes !== metadata.section_bytes
|
||||
)
|
||||
issue(
|
||||
context,
|
||||
["auditable_metadata", "file_offset"],
|
||||
"Dependency section range is outside the selected artifact or disagrees with its compressed size.",
|
||||
);
|
||||
};
|
||||
|
||||
const checkPackages = (
|
||||
metadata: RustAuditableMetadata,
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
const roots: number[] = [];
|
||||
const edgesByPackage = new Map<
|
||||
number,
|
||||
z.infer<typeof dependencyEdgeSchema>[]
|
||||
>();
|
||||
for (const [position, edge] of metadata.dependency_edges.entries()) {
|
||||
if (
|
||||
edge.from_index >= metadata.packages.length ||
|
||||
edge.to_index >= metadata.packages.length
|
||||
) {
|
||||
issue(
|
||||
context,
|
||||
["auditable_metadata", "dependency_edges", position],
|
||||
"Dependency edge index is outside the package list.",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const grouped = edgesByPackage.get(edge.from_index);
|
||||
if (grouped === undefined) edgesByPackage.set(edge.from_index, [edge]);
|
||||
else grouped.push(edge);
|
||||
}
|
||||
for (const [packageIndex, pkg] of metadata.packages.entries()) {
|
||||
if (pkg.root) roots.push(packageIndex);
|
||||
const edges = edgesByPackage.get(packageIndex) ?? [];
|
||||
if (
|
||||
edges.length !== pkg.dependencies.length ||
|
||||
edges.some((edge, index) => edge.to_index !== pkg.dependencies[index])
|
||||
)
|
||||
issue(
|
||||
context,
|
||||
["auditable_metadata", "dependency_edges"],
|
||||
"Dependency edges do not match package dependency indices.",
|
||||
);
|
||||
if (pkg.dependencies.some((target) => target >= metadata.packages.length))
|
||||
issue(
|
||||
context,
|
||||
["auditable_metadata", "packages", packageIndex, "dependencies"],
|
||||
"Dependency index is outside the package list.",
|
||||
);
|
||||
}
|
||||
const root = metadata.root_package_index;
|
||||
if (
|
||||
root === null ? roots.length !== 0 : roots.length !== 1 || roots[0] !== root
|
||||
)
|
||||
issue(
|
||||
context,
|
||||
["auditable_metadata", "root_package_index"],
|
||||
"Root package index does not match packages marked root.",
|
||||
);
|
||||
const cycles = cyclicDependencyIndices(metadata.packages);
|
||||
if (
|
||||
cycles.length !== metadata.cyclic_dependency_indices.length ||
|
||||
cycles.some(
|
||||
(index, position) =>
|
||||
metadata.cyclic_dependency_indices[position] !== index,
|
||||
)
|
||||
)
|
||||
issue(
|
||||
context,
|
||||
["auditable_metadata", "cyclic_dependency_indices"],
|
||||
"Cyclic dependency indices do not match the cycles in package dependencies.",
|
||||
);
|
||||
};
|
||||
|
||||
/** Package indices that participate in a dependency cycle, in ascending order. */
|
||||
export const cyclicDependencyIndices = (
|
||||
packages: readonly { readonly dependencies: readonly number[] }[],
|
||||
): number[] => {
|
||||
const edges = packages.map((pkg) => pkg.dependencies);
|
||||
const order = dependencyPostorder(edges);
|
||||
const reversed = reversedDependencyEdges(edges);
|
||||
const seen = new Uint8Array(edges.length);
|
||||
const cyclic = new Set<number>();
|
||||
for (let cursor = order.length - 1; cursor >= 0; cursor -= 1) {
|
||||
const start = order[cursor];
|
||||
if (start === undefined || seen[start] === 1) continue;
|
||||
const component = dependencyComponent(start, reversed, seen);
|
||||
if (dependencyComponentInCycle(component, edges))
|
||||
for (const index of component) cyclic.add(index);
|
||||
}
|
||||
return [...cyclic].sort((left, right) => left - right);
|
||||
};
|
||||
|
||||
interface DependencyWalkFrame {
|
||||
readonly index: number;
|
||||
edge: number;
|
||||
}
|
||||
|
||||
const dependencyPostorder = (
|
||||
edges: readonly (readonly number[])[],
|
||||
): number[] => {
|
||||
const seen = new Uint8Array(edges.length);
|
||||
const order: number[] = [];
|
||||
for (let start = 0; start < edges.length; start += 1) {
|
||||
if (seen[start] === 1) continue;
|
||||
const stack: DependencyWalkFrame[] = [{ index: start, edge: 0 }];
|
||||
seen[start] = 1;
|
||||
while (stack.length > 0) {
|
||||
const frame = stack[stack.length - 1];
|
||||
if (frame === undefined) break;
|
||||
const next = edges[frame.index]?.[frame.edge];
|
||||
if (next === undefined) {
|
||||
stack.pop();
|
||||
order.push(frame.index);
|
||||
continue;
|
||||
}
|
||||
frame.edge += 1;
|
||||
if (seen[next] === 0) {
|
||||
seen[next] = 1;
|
||||
stack.push({ index: next, edge: 0 });
|
||||
}
|
||||
}
|
||||
}
|
||||
return order;
|
||||
};
|
||||
|
||||
const reversedDependencyEdges = (
|
||||
edges: readonly (readonly number[])[],
|
||||
): number[][] => {
|
||||
const reversed = Array.from({ length: edges.length }, () => [] as number[]);
|
||||
for (let from = 0; from < edges.length; from += 1)
|
||||
for (const to of edges[from] ?? []) reversed[to]?.push(from);
|
||||
return reversed;
|
||||
};
|
||||
|
||||
const dependencyComponent = (
|
||||
start: number,
|
||||
reversed: readonly (readonly number[])[],
|
||||
seen: Uint8Array,
|
||||
): number[] => {
|
||||
const component: number[] = [];
|
||||
const stack = [start];
|
||||
seen[start] = 1;
|
||||
while (stack.length > 0) {
|
||||
const index = stack.pop();
|
||||
if (index === undefined) break;
|
||||
component.push(index);
|
||||
for (const next of reversed[index] ?? []) {
|
||||
if (seen[next] === 0) {
|
||||
seen[next] = 1;
|
||||
stack.push(next);
|
||||
}
|
||||
}
|
||||
}
|
||||
return component;
|
||||
};
|
||||
|
||||
const dependencyComponentInCycle = (
|
||||
component: readonly number[],
|
||||
edges: readonly (readonly number[])[],
|
||||
): boolean => {
|
||||
if (component.length > 1) return true;
|
||||
const only = component[0];
|
||||
return only !== undefined && (edges[only]?.includes(only) ?? false);
|
||||
};
|
||||
|
||||
const checkRustc = (
|
||||
fileBytes: number,
|
||||
commit: RustBinary["rustc_commit"],
|
||||
context: z.RefinementCtx,
|
||||
): void => {
|
||||
if (commit === null) return;
|
||||
const seen = new Set<string>();
|
||||
for (const [position, entry] of commit.hashes.entries()) {
|
||||
if (seen.has(entry.hash) || !inside(fileBytes, entry.file_offset, 48))
|
||||
issue(
|
||||
context,
|
||||
["rustc_commit", "hashes", position],
|
||||
"rustc path hash is duplicated or its file offset is outside the artifact.",
|
||||
);
|
||||
seen.add(entry.hash);
|
||||
}
|
||||
};
|
||||
@@ -70,9 +70,11 @@ const compareDimension = (
|
||||
after: ReadonlyMap<string, string>,
|
||||
complete: boolean,
|
||||
reason: string,
|
||||
inventoryComplete = complete,
|
||||
): Dimension => {
|
||||
// Membership evidence can be complete even when selected content is unknown.
|
||||
const all = compareIdentities(before, after).filter(
|
||||
({ change }) => complete || change === "modified",
|
||||
({ change }) => inventoryComplete || change === "modified",
|
||||
);
|
||||
if (all.length > 0)
|
||||
return {
|
||||
@@ -160,6 +162,7 @@ const storageDimension = (
|
||||
),
|
||||
storageComparable(before, after, keysComplete, usageComplete),
|
||||
"Storage fingerprints, inventories, usage, or quota were unavailable or truncated in at least one observation.",
|
||||
keysComplete,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -683,16 +686,17 @@ const storageMap = (
|
||||
}),
|
||||
],
|
||||
]);
|
||||
if (!includeKeys) return map;
|
||||
const add = (kind: string, keys: readonly string[]) => {
|
||||
for (const key of keys) {
|
||||
map.set(`storage:${kind}:${key}`, digestCanonicalValue(key));
|
||||
}
|
||||
};
|
||||
add("local_storage", storage.local_storage_keys);
|
||||
add("session_storage", storage.session_storage_keys);
|
||||
add("indexed_db", storage.indexed_db_names);
|
||||
add("cache", storage.cache_names);
|
||||
if (includeKeys) {
|
||||
add("local_storage", storage.local_storage_keys);
|
||||
add("session_storage", storage.session_storage_keys);
|
||||
add("indexed_db", storage.indexed_db_names);
|
||||
add("cache", storage.cache_names);
|
||||
}
|
||||
for (const fingerprint of storage.content_fingerprints) {
|
||||
const identity = `${fingerprint.scope}:${fingerprint.identity_sha256}`;
|
||||
if (!fingerprintIdentities.has(identity)) continue;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user