Merge remote-tracking branch 'origin/main' into feat/pkg-inventory
# Conflicts: # docs/product-catalog.json # docs/verification/managed-conformance-manifest.json # skills/reverse-engineer-anything/SKILL.md # src/generatedMcpToolCatalog.ts
This commit is contained in:
commit
667bf164f9
|
|
@ -239,7 +239,7 @@ jobs:
|
|||
if-no-files-found: error
|
||||
|
||||
package-e2e:
|
||||
name: Package/install integration (${{ matrix.os }})
|
||||
name: Native package baseline (${{ matrix.platform }} ${{ matrix.architecture }})
|
||||
needs: [changes, windows-native-build]
|
||||
if: github.event_name == 'push' || needs.changes.outputs.package == 'true'
|
||||
strategy:
|
||||
|
|
@ -253,7 +253,7 @@ jobs:
|
|||
- os: ubuntu-24.04-arm
|
||||
platform: linux
|
||||
architecture: arm64
|
||||
- os: macos-14
|
||||
- os: macos-15
|
||||
platform: darwin
|
||||
architecture: arm64
|
||||
- os: macos-15-intel
|
||||
|
|
@ -286,12 +286,15 @@ jobs:
|
|||
path: native/windows/build/
|
||||
- run: npm run verify:windows-native:artifact
|
||||
- run: npm run verify:package
|
||||
- name: Verify real Node Inspector discovery and observation
|
||||
if: matrix.platform == 'darwin'
|
||||
run: npm run verify:inspector
|
||||
|
||||
apple-artifacts:
|
||||
name: Real Apple artifact CLI/MCP E2E
|
||||
needs: changes
|
||||
if: github.event_name == 'push' || needs.changes.outputs.code == 'true'
|
||||
runs-on: macos-14
|
||||
runs-on: macos-15
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -355,12 +358,6 @@ jobs:
|
|||
- os: ubuntu-24.04-arm
|
||||
platform: linux
|
||||
architecture: arm64
|
||||
- os: macos-14
|
||||
platform: darwin
|
||||
architecture: arm64
|
||||
- os: macos-15-intel
|
||||
platform: darwin
|
||||
architecture: x64
|
||||
- os: windows-latest
|
||||
platform: win32
|
||||
architecture: x64
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@
|
|||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
[Documentation](https://morluto.github.io/rea/) · [Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
|
||||
**[Website](https://morluto.github.io/rea/) · [Guides](https://morluto.github.io/rea/guides/) · [DX-Ball showcase](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[الموقع (بالإنجليزية)](https://morluto.github.io/rea/) · [أدلة الاستخدام](https://morluto.github.io/rea/guides/) · [دراسة حالة DX-Ball](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
|
||||
|
||||
<table aria-label="REA community">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[ウェブサイト(英語)](https://morluto.github.io/rea/) · [ガイド](https://morluto.github.io/rea/guides/) · [DX-Ball の事例](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
|
||||
|
||||
<table aria-label="REA community">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[웹사이트(영문)](https://morluto.github.io/rea/) · [가이드](https://morluto.github.io/rea/guides/) · [DX-Ball 사례](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
|
||||
|
||||
<table aria-label="REA community">
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[网站(英文)](https://morluto.github.io/rea/) · [使用指南](https://morluto.github.io/rea/guides/) · [DX-Ball 案例](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
|
||||
|
||||
<table aria-label="REA community">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,11 @@
|
|||
struct ChildBatch<Element> {
|
||||
let values: [Element]
|
||||
let complete: Bool
|
||||
}
|
||||
|
||||
func captureChildValues<Element>(requestedCount: Int, copy: () -> [Element]?) -> ChildBatch<Element> {
|
||||
guard requestedCount > 0 else { return ChildBatch(values: [], complete: true) }
|
||||
let returned = copy() ?? []
|
||||
let values = Array(returned.prefix(requestedCount))
|
||||
return ChildBatch(values: values, complete: values.count == requestedCount)
|
||||
}
|
||||
|
|
@ -26,11 +26,12 @@ func childCount(_ element: AXUIElement) -> Int {
|
|||
var count = 0
|
||||
return AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) == .success ? count : 0
|
||||
}
|
||||
func children(_ element: AXUIElement, limit: Int) -> [AXUIElement] {
|
||||
var value: CFArray?
|
||||
let count = min(limit, childCount(element))
|
||||
if count == 0 { return [] }
|
||||
return AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, 0, count, &value) == .success ? value as? [AXUIElement] ?? [] : []
|
||||
func children(_ element: AXUIElement, count: Int) -> ChildBatch<AXUIElement> {
|
||||
captureChildValues(requestedCount: count) {
|
||||
var value: CFArray?
|
||||
guard AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, 0, count, &value) == .success else { return nil }
|
||||
return value as? [AXUIElement]
|
||||
}
|
||||
}
|
||||
func text(_ element: AXUIElement, _ key: String) -> Any { (attribute(element, key) as? String) ?? NSNull() as Any }
|
||||
func windowBounds(_ element: AXUIElement) -> CGRect? {
|
||||
|
|
@ -115,9 +116,12 @@ func observe(_ request: Request) async throws -> [String: Any] {
|
|||
if path.count >= 32 { if totalChildren > 0 { truncated = true }; continue }
|
||||
let available = max(0, request.max_nodes - nodes.count - pending.count)
|
||||
let count = min(available, totalChildren)
|
||||
let items = children(element, limit: count)
|
||||
let batch = children(element, count: count)
|
||||
if !batch.complete { truncated = true }
|
||||
if totalChildren > count { truncated = true }
|
||||
for index in (0..<count).reversed() { pending.append((items[index], path + [index])) }
|
||||
for (index, item) in batch.values.enumerated().reversed() {
|
||||
pending.append((item, path + [index]))
|
||||
}
|
||||
}
|
||||
}
|
||||
var screenshot: Any = NSNull()
|
||||
|
|
|
|||
|
|
@ -96,9 +96,11 @@ The components also list:
|
|||
|
||||
Symlinks are reported by path in `symlinks`. Their targets are not inventoried,
|
||||
so a versioned framework is described through its real `Versions/<version>/`
|
||||
directories rather than `Versions/Current`. When a framework has zero or several
|
||||
`Versions/*/Resources/Info.plist` files, its `info_plist_path` is `null` and a
|
||||
limitation explains why.
|
||||
directories rather than `Versions/Current`. A framework's `info_plist_path` is
|
||||
`null`, with a limitation explaining why, when it has more than one real
|
||||
`Versions/<version>/` directory (even if only one of them holds a plist,
|
||||
because `Versions/Current` decides which one applies) or when its single
|
||||
version directory has no `Resources/Info.plist`.
|
||||
|
||||
AppleDouble sidecar files (`._*` and `__MACOSX/`) are inventory facts, but they
|
||||
describe neighbouring files. They are excluded from roots, bundle roles, and the
|
||||
|
|
|
|||
|
|
@ -295,6 +295,30 @@ lifecycles. Runtime ports retain their distinct Inspector, passive Electron and
|
|||
active Electron effects; shipped Electron hooks remain at their existing
|
||||
provider boundary.
|
||||
|
||||
## Managed capability ownership
|
||||
|
||||
`src/domain/managed/` owns managed artifact and member semantics, reconstruction
|
||||
import, native-boundary verification and application graph projection.
|
||||
`src/application/managed/` owns their shared Evidence workflows, and
|
||||
`src/contracts/managed/` owns their named contracts and examples. Producer
|
||||
metadata/IL parsing stays in `src/dotnet/`; target resolution, Evidence, ordering
|
||||
and investigation records retain their shared owners. Path-based member
|
||||
comparison preserves its actual target/byte-digest admission and existing parser
|
||||
helper.
|
||||
|
||||
`scripts/verify/managed/` groups the portable conformance entrypoint and its
|
||||
support, manifest, oracle and completion-report helpers. Its PE byte fixture
|
||||
builder and declaration live in `scripts/fixtures/managed/`, shared with the
|
||||
installed-package lane and existing managed conformance tests. `verify:managed`
|
||||
keeps its command name and optional caller-cwd manifest resolution. Compiled
|
||||
catalog and generator imports follow the owners; document-check cache inputs
|
||||
include the relocated runtime verifier and fixture files.
|
||||
|
||||
Portable byte fixtures, optional operator-local manifests, actual ILSpy oracles
|
||||
and real Ghidra NativeAOT checks retain their distinct proof levels and
|
||||
prerequisites. The existing NativeAOT fixture/engine lane keeps its owner. This
|
||||
move preserves implementations, Evidence identities and generated commitments.
|
||||
|
||||
## Binary application ownership
|
||||
|
||||
`src/application/binary/` owns the active session, deep-provider registry and
|
||||
|
|
|
|||
|
|
@ -39,9 +39,20 @@ explicit limitation; architecture inventory locations remain available.
|
|||
vtable descriptors. Records include exact virtual addresses/file offsets and
|
||||
artifact evidence. Vtable indexes are metadata word offsets; witness indexes
|
||||
start after the conformance header. Names unavailable in metadata remain null.
|
||||
Chained fixups, external bindings, authenticated pointers, categories,
|
||||
properties, generic/resilient/async/coroutine tables and inherited overrides
|
||||
have explicit unsupported or partial coverage. Other providers retain the
|
||||
Pointers are decoded through the image's fixups:
|
||||
- `LC_DYLD_CHAINED_FIXUPS` rebases and binds, including the authenticated
|
||||
arm64e formats;
|
||||
- legacy `LC_DYLD_INFO` bind opcodes.
|
||||
|
||||
So an external superclass resolves from its `_OBJC_CLASS_$_` bind (for
|
||||
example `NSObject`). The `pointer_fixups` coverage facet names the mechanism.
|
||||
Class properties, with parsed attributes, and `__objc_catlist` categories are
|
||||
also decoded. Categories record the extended class, local or external, plus
|
||||
their methods, protocols and properties; category methods appear as
|
||||
implementations with a `category`. Swift field-offset globals that are only
|
||||
initialized at runtime stay unresolved. Generic, resilient, async and
|
||||
coroutine tables, and inherited overrides, have explicit unsupported or
|
||||
partial coverage. Other providers retain the
|
||||
existing symbol-based inventory with its narrower coverage.
|
||||
|
||||
## Native instruction, call and type primitives
|
||||
|
|
@ -165,8 +176,10 @@ GUI authority, and REA never falls back automatically to Hopper.
|
|||
instruction/call facts, value dependencies and process/project cleanup.
|
||||
- `npm run verify:ghidra:aarch64-jump-table`: optimized ELF and byte/halfword
|
||||
relative tables, plus ARM64 Mach-O on an ARM64 macOS host.
|
||||
- `npm run verify:apple-dispatch`: source-built Objective-C protocols/classes and
|
||||
Swift conformances/vtables, repeated after stripping local symbols.
|
||||
- `npm run verify:apple-dispatch`: source-built Objective-C protocols, classes,
|
||||
properties and categories, and Swift conformances and vtables. Each is linked
|
||||
with legacy `LC_DYLD_INFO` and with chained fixups (and as arm64e on Apple
|
||||
silicon), then repeated after stripping local symbols.
|
||||
- `npm run verify:native-ui`: one source-owned fixture window, successful
|
||||
selected-window capture and actions, changed-target rejection, and cleanup.
|
||||
Missing OS permissions fail this lane.
|
||||
|
|
|
|||
|
|
@ -669,9 +669,9 @@
|
|||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "620bc21113bcc2ed5775f3bdb0a59a3d6208e8aec9d918f220b5c7c85e9e4dd6",
|
||||
"tools_sha256": "3381610341f922f75fdc2e2201fe54e66e7f431fee3b4316c95d85e2534749b8",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "d18386ad8f46e7035cfa49f075c42e5c143f5b3f05c9459eb2d1c23c11112871",
|
||||
"combined_sha256": "ab7efdea56f2e319bafbf1e1fc7d8ff998f3da1845968acb1614fc5be4dd6250",
|
||||
"providers_sha256": "a7d885531ee25d42c51385444faec544d34cd7eba6250a7dc75c4faedd4454a3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,16 @@ needed, exercise the actual target through browser, Electron, or process
|
|||
capture. Real model trials are manual; Vitest covers deterministic evaluator
|
||||
logic.
|
||||
|
||||
`verify:managed` runs the portable PE byte-fixture conformance entrypoint under
|
||||
`scripts/verify/managed/`, with its byte builder under
|
||||
`scripts/fixtures/managed/`. It checks static classification, members,
|
||||
reconstruction, native-boundary relationships and application graphs without
|
||||
executing fixture PE files. Operator-local manifests and actual ILSpy oracles
|
||||
remain optional, separately reported checks; the real Ghidra NativeAOT lane has
|
||||
its own toolchain prerequisites. See [the managed guide](managed-code-analysis.md)
|
||||
for those configurations. Generated completion-ledger checks use the same owning
|
||||
entrypoint and include its verifier/fixture files in their cache inputs.
|
||||
|
||||
## End-to-end, integration and golden evidence
|
||||
|
||||
Full E2E tests invoke the production command dispatcher and real providers,
|
||||
|
|
@ -335,8 +345,8 @@ CI exercises the pinned Node.js runtime on native hosted runners:
|
|||
| --------------------- | ------------------ | ---------------------------------------------------------------------------------------- |
|
||||
| Linux x64 | `ubuntu-latest` | Installed package and real Node Inspector CLI/MCP |
|
||||
| Linux arm64 (aarch64) | `ubuntu-24.04-arm` | Installed package and real Node Inspector CLI/MCP |
|
||||
| macOS arm64 | `macos-14` | Installed package and real Node Inspector CLI/MCP |
|
||||
| macOS x64 | `macos-15-intel` | Installed package and real Node Inspector CLI/MCP |
|
||||
| macOS 15 arm64 | `macos-15` | Installed package and real Node Inspector CLI/MCP |
|
||||
| macOS 15 x64 | `macos-15-intel` | Installed package and real Node Inspector CLI/MCP |
|
||||
| Windows x64 | `windows-latest` | Curated capabilities, native controls, installed package and real Node Inspector CLI/MCP |
|
||||
|
||||
Package and Inspector matrices assert the actual Node platform/architecture
|
||||
|
|
@ -346,6 +356,11 @@ limits. Package checks cover installation, CLI/MCP discovery, target-free
|
|||
analysis, configuration backups/recovery, Evidence and owned lifecycle; Inspector
|
||||
checks execute source-owned loopback targets and special filename cases.
|
||||
|
||||
macOS uses one OS version with one native baseline job per architecture. Each job
|
||||
runs package and Inspector checks after a single dependency installation. The
|
||||
separate Inspector matrix covers Linux and Windows; Apple artifact checks retain
|
||||
their own macOS 15 arm64 job for Xcode-dependent workflows.
|
||||
|
||||
These native baseline checks complement the Linux source-test shards and the
|
||||
separate Apple-artifact and real-provider lanes. Actual Hopper, Ghidra, IDA,
|
||||
browser and managed-tool claims require their corresponding verification lanes.
|
||||
|
|
@ -456,17 +471,24 @@ that explicit lane, not the routine iteration requirement.
|
|||
|
||||
## Apple native metadata and UI
|
||||
|
||||
`npm run verify:apple-dispatch` compiles Objective-C class/protocol and Swift
|
||||
conformance/vtable fixtures, inspects their bytes and repeats after stripping
|
||||
local symbols. It requires macOS and the host Xcode toolchain; targets are not
|
||||
executed. `npm run verify:native-ui` launches exactly one source-owned fixture
|
||||
window and requires successful selected-window capture and selected actions.
|
||||
An OS permission denial fails the positive lane. `npm run verify:native-ui:permissions`
|
||||
allows a host-permission-boundary-only result and explicitly reports
|
||||
`positive_e2e: false`; it must not be reported as capture/action proof.
|
||||
Both commands reject a changed executable digest and clean up the fixture
|
||||
process and helper. These lanes require an interactive macOS desktop. See [native investigation](native-investigation.md)
|
||||
for the exact ABI, authority, graph and observation boundaries.
|
||||
`npm run verify:apple-dispatch` compiles the Objective-C fixture (classes,
|
||||
protocols, a property and an `NSString` category) and the Swift
|
||||
conformance/vtable fixture.
|
||||
|
||||
- Each fixture is linked with legacy `LC_DYLD_INFO` binds and with chained
|
||||
fixups; on Apple silicon the ObjC fixture is also built as arm64e, which uses
|
||||
authenticated pointers.
|
||||
- The lane inspects the bytes and repeats after stripping local symbols.
|
||||
- It requires the bound `NSObject` superclass, the external category, and the
|
||||
matching `pointer_fixups` coverage. It requires macOS and the host Xcode toolchain; targets are not
|
||||
executed. `npm run verify:native-ui` launches exactly one source-owned fixture
|
||||
window and requires successful selected-window capture and selected actions.
|
||||
An OS permission denial fails the positive lane. `npm run verify:native-ui:permissions`
|
||||
allows a host-permission-boundary-only result and explicitly reports
|
||||
`positive_e2e: false`; it must not be reported as capture/action proof.
|
||||
Both commands reject a changed executable digest and clean up the fixture
|
||||
process and helper. These lanes require an interactive macOS desktop. See [native investigation](native-investigation.md)
|
||||
for the exact ABI, authority, graph and observation boundaries.
|
||||
|
||||
### Firmware adapters
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"manifest_id": "ecm_cba6ab6b66603cd59aa60b327d67efe09bb9d004c73d1c7e8e368d86887c686e",
|
||||
"manifest_id": "ecm_9b3d014e67d8c2ccc8736694d59b38acf6d8ab15d1e9cdeb3998339ff33b5cc2",
|
||||
"verifier": {
|
||||
"id": "rea-managed-conformance",
|
||||
"version": "1"
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"skill_digests": [
|
||||
{
|
||||
"skill_id": "reverse-engineer-anything",
|
||||
"sha256": "21bd8b88040d593111905bb47863f5145690c4d5582e0975bab76d2b07fb25b9"
|
||||
"sha256": "17120894d8defd8c28791e344b9241e8d3abcba5ac345b0c8cca92adda518dfd"
|
||||
}
|
||||
],
|
||||
"claims": [
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
"vitepress": {
|
||||
"entry": ["docs/.vitepress/config.ts"]
|
||||
},
|
||||
"ignoreBinaries": ["mkfifo"],
|
||||
"ignoreDependencies": ["electron", "esbuild", "webpack"],
|
||||
"ignoreExportsUsedInFile": true,
|
||||
"rules": {
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@
|
|||
"dist",
|
||||
"install.sh",
|
||||
"bridge/hopper_bridge.py",
|
||||
"bridge/native/NativeUIChildren.swift",
|
||||
"bridge/native/ReaNativeUI.swift",
|
||||
"bridge/ghidra/ReaGhidraBridge.java",
|
||||
"bridge/ghidra/ReaGhidraExtensions.java",
|
||||
|
|
@ -142,7 +143,7 @@
|
|||
"verify:javascript-runtime": "npm run build:cached && node scripts/verify/javascript/runtime-observation.mjs",
|
||||
"verify:javascript:digests": "npm run build:cached && node scripts/verify/javascript/digests.mjs",
|
||||
"verify:application-workflows": "npm run build:cached && node scripts/verify/javascript/application-workflows.mjs",
|
||||
"verify:managed": "npm run build:cached && node scripts/verify-managed-conformance.mjs",
|
||||
"verify:managed": "npm run build:cached && node scripts/verify/managed/conformance.mjs",
|
||||
"verify:firmware": "npm run build:cached && node scripts/verify/firmware/analysis.mjs",
|
||||
"fixtures:firmware": "node scripts/fixtures/firmware/generate.mjs",
|
||||
"verify:android": "npm run build:cached && node scripts/verify/android/analysis.mjs",
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ delete verifierEnvironment.REA_ILSPY_CMD_PATH;
|
|||
|
||||
const { stdout } = await exec(
|
||||
process.execPath,
|
||||
[join(root, "scripts/verify-managed-conformance.mjs")],
|
||||
[join(root, "scripts/verify/managed/conformance.mjs")],
|
||||
{
|
||||
cwd: root,
|
||||
env: verifierEnvironment,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { Ajv2020 } from "ajv/dist/2020.js";
|
|||
import { McpServer } from "@modelcontextprotocol/server";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managedWorkflowToolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managed/managedWorkflowToolContracts.js";
|
||||
import { auxiliaryAnalysisProviderDeclarations } from "../dist/composition/auxiliaryAnalysisProviders.js";
|
||||
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
|
||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ import time
|
|||
# Keep this digest aligned with LinuxHopper.ts and rerun real-Hopper verification
|
||||
# before accepting another vendor build.
|
||||
SUPPORTED_HOPPER_SHA256 = {
|
||||
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded"
|
||||
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded",
|
||||
"1339f9e58377442b0c6fcb0dfc3cec20d593cc557408521fad9a00dbc6b8da13",
|
||||
}
|
||||
EXPECTED_SCREEN = (1280, 1024)
|
||||
EXPECTED_DIALOG = (189, 370, 901, 284)
|
||||
|
|
|
|||
|
|
@ -42,12 +42,13 @@ const SOURCE_PATHS = {
|
|||
sessionContracts: "dist/contracts/sessionToolContracts.js",
|
||||
nativeContracts: "dist/contracts/nativeToolContracts.js",
|
||||
artifactContracts: "dist/contracts/artifactToolContracts.js",
|
||||
managedContracts: "dist/contracts/managedToolContracts.js",
|
||||
managedContracts: "dist/contracts/managed/managedToolContracts.js",
|
||||
firmwareContracts: "dist/contracts/firmware/firmwareToolContracts.js",
|
||||
firmwareProvider: "dist/firmware/FirmwareRelease.js",
|
||||
androidContracts: "dist/contracts/android/androidToolContracts.js",
|
||||
androidProvider: "dist/android/JadxRelease.js",
|
||||
managedWorkflowContracts: "dist/contracts/managedWorkflowToolContracts.js",
|
||||
managedWorkflowContracts:
|
||||
"dist/contracts/managed/managedWorkflowToolContracts.js",
|
||||
browserContracts: "dist/contracts/browserToolContracts.js",
|
||||
browserScenarioContracts: "dist/contracts/browserScenarioToolContracts.js",
|
||||
electronContracts: "dist/contracts/javascript/electronToolContracts.js",
|
||||
|
|
@ -88,9 +89,9 @@ const SOURCE_PATHS = {
|
|||
"dist/domain/javascript/javascriptRuntimeObservation.js",
|
||||
webBundleAnalysis: "dist/domain/webBundleAnalysis.js",
|
||||
webCaptureDiff: "dist/domain/webCaptureDiff.js",
|
||||
managedArtifact: "dist/domain/managedArtifact.js",
|
||||
managedComparison: "dist/domain/managedMemberComparison.js",
|
||||
managedNativeVerification: "dist/domain/managedNativeVerification.js",
|
||||
managedArtifact: "dist/domain/managed/managedArtifact.js",
|
||||
managedComparison: "dist/domain/managed/managedMemberComparison.js",
|
||||
managedNativeVerification: "dist/domain/managed/managedNativeVerification.js",
|
||||
webMcpDiscovery: "dist/domain/webMcpDiscovery.js",
|
||||
webScreenshot: "dist/domain/webScreenshot.js",
|
||||
javascriptApplicationGraph:
|
||||
|
|
|
|||
|
|
@ -12,118 +12,190 @@ if (process.platform !== "darwin")
|
|||
"Apple dispatch verification requires macOS with a host Objective-C compiler and Foundation SDK",
|
||||
);
|
||||
const exec = promisify(execFile);
|
||||
const source = (name) =>
|
||||
fileURLToPath(
|
||||
new URL(`../tests/conformance/native/${name}`, import.meta.url),
|
||||
);
|
||||
|
||||
/** Both link modes: legacy LC_DYLD_INFO binds and the default chained fixups. */
|
||||
const LINK_MODES = [
|
||||
{
|
||||
mode: "dyld-info",
|
||||
flags: ["-Wl,-no_fixup_chains"],
|
||||
fixups: "LC_DYLD_INFO bind opcodes",
|
||||
swift: true,
|
||||
},
|
||||
{ mode: "chained", flags: [], fixups: "chained fixups:", swift: true },
|
||||
// arm64e uses authenticated chained pointers (DYLD_CHAINED_PTR_ARM64E_*).
|
||||
...(process.arch === "arm64"
|
||||
? [
|
||||
{
|
||||
mode: "chained-arm64e",
|
||||
flags: ["-arch", "arm64e"],
|
||||
fixups: "chained fixups: DYLD_CHAINED_PTR_ARM64E",
|
||||
swift: false,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const fixupCoverage = (metadata) =>
|
||||
metadata.coverage.find(({ facet }) => facet === "pointer_fixups");
|
||||
|
||||
const inspect = async (path) => {
|
||||
const target = await parseBinaryTarget(path);
|
||||
if (!target.ok) throw target.error;
|
||||
return {
|
||||
target: target.value,
|
||||
metadata: (await inspectAppleDispatchMetadata(target.value, 5000)).result,
|
||||
};
|
||||
};
|
||||
|
||||
const checkObjc = (metadata, mode) => {
|
||||
const fixture = metadata.objc_classes.find(
|
||||
({ name, is_meta_class: meta }) => name === "ReaDispatchFixture" && !meta,
|
||||
);
|
||||
const category = metadata.objc_categories.find(
|
||||
({ name }) => name === "ReaDispatchAdditions",
|
||||
);
|
||||
const objcCoverage = metadata.coverage.find(
|
||||
({ facet }) => facet === "objc_class_method_ivar_metadata",
|
||||
);
|
||||
if (
|
||||
!metadata.objc_dispatch_implementations.some(
|
||||
(item) =>
|
||||
item.class_name === "ReaDispatchFixture" &&
|
||||
item.selector === "performAction:" &&
|
||||
item.implementation_address !== null,
|
||||
) ||
|
||||
!metadata.objc_dispatch_implementations.some(
|
||||
(item) =>
|
||||
item.selector === "fixtureVersion" && item.method_type === "class",
|
||||
) ||
|
||||
!metadata.objc_ivars.some(
|
||||
(item) => item.name === "state" && item.offset !== null,
|
||||
) ||
|
||||
!metadata.objc_protocol_records.some(
|
||||
(protocol) =>
|
||||
protocol.name === "ReaDispatchProtocol" &&
|
||||
protocol.methods.some(
|
||||
(method) => method.selector === "performAction:",
|
||||
) &&
|
||||
protocol.optional_methods.some(
|
||||
(method) => method.selector === "optionalFixtureValue",
|
||||
),
|
||||
) ||
|
||||
// The superclass lives in libobjc, so it comes from a bind fixup.
|
||||
fixture?.super_class !== "NSObject" ||
|
||||
fixture.decode.status !== "decoded" ||
|
||||
!fixture.properties.some(
|
||||
(property) =>
|
||||
property.name === "fixtureName" &&
|
||||
property.type_encoding === '@"NSString"' &&
|
||||
property.attributes.some(({ name }) => name === "C"),
|
||||
) ||
|
||||
category?.class_name !== "NSString" ||
|
||||
category.class_source !== "external" ||
|
||||
!category.instance_methods.includes("reaFixtureLength") ||
|
||||
!metadata.objc_dispatch_implementations.some(
|
||||
(item) =>
|
||||
item.class_name === "NSString" &&
|
||||
item.category === "ReaDispatchAdditions" &&
|
||||
item.selector === "reaFixtureLength",
|
||||
) ||
|
||||
objcCoverage?.status !== "complete" ||
|
||||
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true
|
||||
)
|
||||
throw new Error(
|
||||
`Apple metadata fixture drifted (${mode.mode}): ${JSON.stringify(metadata)}`,
|
||||
);
|
||||
};
|
||||
|
||||
const checkSwift = (metadata, mode) => {
|
||||
if (
|
||||
!metadata.swift_dispatch_slots.some(
|
||||
(slot) =>
|
||||
slot.table_kind === "class_vtable" &&
|
||||
slot.owner === "ReaVtableFixture" &&
|
||||
slot.decode.status === "decoded",
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
`Swift class vtable fixture unavailable (${mode.mode}): ${JSON.stringify(metadata.coverage)}`,
|
||||
);
|
||||
if (
|
||||
!metadata.swift_conformances.some(
|
||||
(item) =>
|
||||
item.type_name === "ReaWitnessFixture" &&
|
||||
item.protocol_name === "ReaWitnessFixtureProtocol",
|
||||
) ||
|
||||
!metadata.swift_dispatch_slots.some(
|
||||
(item) =>
|
||||
item.owner === "ReaWitnessFixture: ReaWitnessFixtureProtocol" &&
|
||||
item.implementation_address !== null,
|
||||
) ||
|
||||
fixupCoverage(metadata)?.reason?.startsWith(mode.fixups) !== true
|
||||
)
|
||||
throw new Error(
|
||||
`Swift metadata fixture drifted (${mode.mode}): ${JSON.stringify(metadata)}`,
|
||||
);
|
||||
};
|
||||
|
||||
const root = await mkdtemp(join(tmpdir(), "rea-dispatch-"));
|
||||
try {
|
||||
const targetPath = join(root, "fixture");
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"clang",
|
||||
"-O2",
|
||||
"-framework",
|
||||
"Foundation",
|
||||
"-Wl,-no_fixup_chains",
|
||||
fileURLToPath(
|
||||
new URL("../tests/conformance/native/dispatch.m", import.meta.url),
|
||||
),
|
||||
"-o",
|
||||
targetPath,
|
||||
]);
|
||||
const results = [];
|
||||
for (const variant of ["symbols", "stripped"]) {
|
||||
if (variant === "stripped")
|
||||
await exec("/usr/bin/strip", ["-x", targetPath]);
|
||||
const target = await parseBinaryTarget(targetPath);
|
||||
if (!target.ok) throw target.error;
|
||||
const observation = await inspectAppleDispatchMetadata(target.value, 5000);
|
||||
const metadata = observation.result;
|
||||
if (
|
||||
!metadata.objc_dispatch_implementations.some(
|
||||
(item) =>
|
||||
item.class_name === "ReaDispatchFixture" &&
|
||||
item.selector === "performAction:" &&
|
||||
item.implementation_address !== null,
|
||||
) ||
|
||||
!metadata.objc_dispatch_implementations.some(
|
||||
(item) =>
|
||||
item.selector === "fixtureVersion" && item.method_type === "class",
|
||||
) ||
|
||||
!metadata.objc_ivars.some(
|
||||
(item) => item.name === "state" && item.offset !== null,
|
||||
) ||
|
||||
!metadata.objc_protocol_records.some(
|
||||
(protocol) =>
|
||||
protocol.name === "ReaDispatchProtocol" &&
|
||||
protocol.methods.some(
|
||||
(method) => method.selector === "performAction:",
|
||||
) &&
|
||||
protocol.optional_methods.some(
|
||||
(method) => method.selector === "optionalFixtureValue",
|
||||
),
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
`Apple metadata fixture drifted: ${JSON.stringify(observation)}`,
|
||||
);
|
||||
results.push({
|
||||
variant,
|
||||
target_sha256: target.value.sha256,
|
||||
implementations: metadata.objc_dispatch_implementations.length,
|
||||
ivars: metadata.objc_ivars.length,
|
||||
coverage: metadata.coverage,
|
||||
});
|
||||
}
|
||||
const swiftPath = join(root, "libReaWitnessFixture.dylib");
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"swiftc",
|
||||
"-emit-library",
|
||||
"-O",
|
||||
"-Xlinker",
|
||||
"-no_fixup_chains",
|
||||
fileURLToPath(
|
||||
new URL("../tests/conformance/native/dispatch.swift", import.meta.url),
|
||||
),
|
||||
"-o",
|
||||
swiftPath,
|
||||
]);
|
||||
for (const variant of ["swift-symbols", "swift-stripped"]) {
|
||||
if (variant === "swift-stripped")
|
||||
await exec("/usr/bin/strip", ["-x", swiftPath]);
|
||||
const target = await parseBinaryTarget(swiftPath);
|
||||
if (!target.ok) throw target.error;
|
||||
const observation = await inspectAppleDispatchMetadata(target.value, 5000);
|
||||
const metadata = observation.result;
|
||||
if (
|
||||
!metadata.swift_dispatch_slots.some(
|
||||
(slot) =>
|
||||
slot.table_kind === "class_vtable" &&
|
||||
slot.owner === "ReaVtableFixture" &&
|
||||
slot.decode.status === "decoded",
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
`Swift class vtable fixture unavailable: ${JSON.stringify(metadata.coverage)}`,
|
||||
);
|
||||
if (
|
||||
!metadata.swift_conformances.some(
|
||||
(item) =>
|
||||
item.type_name === "ReaWitnessFixture" &&
|
||||
item.protocol_name === "ReaWitnessFixtureProtocol",
|
||||
) ||
|
||||
!metadata.swift_dispatch_slots.some(
|
||||
(item) =>
|
||||
item.owner === "ReaWitnessFixture: ReaWitnessFixtureProtocol" &&
|
||||
item.implementation_address !== null,
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
`Swift metadata fixture drifted: ${JSON.stringify(observation)}`,
|
||||
);
|
||||
results.push({
|
||||
variant,
|
||||
target_sha256: target.value.sha256,
|
||||
conformances: metadata.swift_conformances.length,
|
||||
slots: metadata.swift_dispatch_slots.length,
|
||||
coverage: metadata.coverage,
|
||||
});
|
||||
for (const mode of LINK_MODES) {
|
||||
const objcPath = join(root, `fixture-${mode.mode}`);
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"clang",
|
||||
"-O2",
|
||||
"-framework",
|
||||
"Foundation",
|
||||
...mode.flags,
|
||||
source("dispatch.m"),
|
||||
"-o",
|
||||
objcPath,
|
||||
]);
|
||||
for (const variant of ["symbols", "stripped"]) {
|
||||
if (variant === "stripped")
|
||||
await exec("/usr/bin/strip", ["-x", objcPath]);
|
||||
const { target, metadata } = await inspect(objcPath);
|
||||
checkObjc(metadata, mode);
|
||||
results.push({
|
||||
variant: `objc-${mode.mode}-${variant}`,
|
||||
target_sha256: target.sha256,
|
||||
classes: metadata.objc_classes.length,
|
||||
categories: metadata.objc_categories.length,
|
||||
implementations: metadata.objc_dispatch_implementations.length,
|
||||
fixups: fixupCoverage(metadata)?.reason,
|
||||
});
|
||||
}
|
||||
if (!mode.swift) continue;
|
||||
const swiftPath = join(root, `libReaWitnessFixture-${mode.mode}.dylib`);
|
||||
await exec("/usr/bin/xcrun", [
|
||||
"swiftc",
|
||||
"-emit-library",
|
||||
"-O",
|
||||
...mode.flags
|
||||
.map((flag) => flag.replace(/^-Wl,/u, "-Xlinker\0"))
|
||||
.flatMap((flag) => flag.split("\0")),
|
||||
source("dispatch.swift"),
|
||||
"-o",
|
||||
swiftPath,
|
||||
]);
|
||||
for (const variant of ["symbols", "stripped"]) {
|
||||
if (variant === "stripped")
|
||||
await exec("/usr/bin/strip", ["-x", swiftPath]);
|
||||
const { target, metadata } = await inspect(swiftPath);
|
||||
checkSwift(metadata, mode);
|
||||
results.push({
|
||||
variant: `swift-${mode.mode}-${variant}`,
|
||||
target_sha256: target.sha256,
|
||||
conformances: metadata.swift_conformances.length,
|
||||
slots: metadata.swift_dispatch_slots.length,
|
||||
fixups: fixupCoverage(metadata)?.reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({ ok: true, target_executed: false, fixtures: results })}\n`,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,23 @@ const root = await mkdtemp(join(tmpdir(), "rea-ui-fixture-"));
|
|||
let child;
|
||||
let fixturePid;
|
||||
try {
|
||||
const childRetrievalTest = join(root, "child-retrieval-test");
|
||||
await promisify(execFile)("/usr/bin/xcrun", [
|
||||
"swiftc",
|
||||
fileURLToPath(
|
||||
new URL("../bridge/native/NativeUIChildren.swift", import.meta.url),
|
||||
),
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
"../tests/conformance/native/native-ui-children/main.swift",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
"-o",
|
||||
childRetrievalTest,
|
||||
]);
|
||||
await promisify(execFile)(childRetrievalTest, []);
|
||||
|
||||
const contents = join(root, "Fixture.app", "Contents");
|
||||
await mkdir(join(contents, "MacOS"), { recursive: true });
|
||||
await writeFile(
|
||||
|
|
@ -82,7 +99,6 @@ try {
|
|||
"capture_native_ui_scenario",
|
||||
{
|
||||
...scope,
|
||||
restore: "leave-as-is",
|
||||
screenshot: false,
|
||||
steps: [
|
||||
{ kind: "click", path: button.path },
|
||||
|
|
@ -111,8 +127,6 @@ try {
|
|||
String(scope.pid),
|
||||
"--window-id",
|
||||
String(scope.window_id),
|
||||
"--restore",
|
||||
"leave-as-is",
|
||||
"--steps",
|
||||
JSON.stringify([
|
||||
{ kind: "click", path: button.path },
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { writeFile } from "node:fs/promises";
|
|||
import { join } from "node:path";
|
||||
|
||||
import { createEvidence } from "../dist/domain/evidence.js";
|
||||
import { buildManagedPeFixture } from "./lib/managed-pe-fixture.mjs";
|
||||
import { buildManagedPeFixture } from "./fixtures/managed/pe.mjs";
|
||||
import { functionDossier, json, run } from "./lib/verify-package-core.mjs";
|
||||
|
||||
const buildManagedFixtures = async (workspace) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { createEvidence } from "../../dist/domain/evidence.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
|
||||
const COMPLETION_PROVIDER = {
|
||||
id: "rea-managed-conformance",
|
||||
|
|
@ -3,21 +3,21 @@ import { tmpdir } from "node:os";
|
|||
import { join } from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { inspectManagedArtifactBytes } from "../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { inspectManagedNativeBoundariesBytes } from "../dist/dotnet/ManagedNativeBoundaryInspector.js";
|
||||
import { compareManagedMemberPaths } from "../dist/application/ManagedMemberComparisonService.js";
|
||||
import { verifyManagedNativeBoundariesEvidence } from "../dist/application/ManagedNativeVerificationService.js";
|
||||
import { importManagedReconstructionEvidence } from "../dist/application/ManagedReconstructionService.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../dist/application/ManagedApplicationGraphService.js";
|
||||
import { traceApplicationFeatureEvidence } from "../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../dist/application/InvestigationProviders.js";
|
||||
import { createEvidence } from "../dist/domain/evidence.js";
|
||||
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { inspectManagedNativeBoundariesBytes } from "../../../dist/dotnet/ManagedNativeBoundaryInspector.js";
|
||||
import { compareManagedMemberPaths } from "../../../dist/application/managed/ManagedMemberComparisonService.js";
|
||||
import { verifyManagedNativeBoundariesEvidence } from "../../../dist/application/managed/ManagedNativeVerificationService.js";
|
||||
import { importManagedReconstructionEvidence } from "../../../dist/application/managed/ManagedReconstructionService.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../../../dist/application/managed/ManagedApplicationGraphService.js";
|
||||
import { traceApplicationFeatureEvidence } from "../../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
import {
|
||||
alternateMvid,
|
||||
buildNativePeFixture,
|
||||
} from "./lib/managed-pe-fixture.mjs";
|
||||
import { createManagedConformanceSupport } from "./lib/managed-conformance-support.mjs";
|
||||
} from "../../fixtures/managed/pe.mjs";
|
||||
import { createManagedConformanceSupport } from "./support.mjs";
|
||||
import {
|
||||
comparisonLimits,
|
||||
defaultIlBody,
|
||||
|
|
@ -25,9 +25,12 @@ import {
|
|||
inspectionLimits,
|
||||
memberLimits,
|
||||
nativeBoundaryLimits,
|
||||
} from "./lib/managed-conformance-config.mjs";
|
||||
import { createManagedCompletionReport } from "./lib/managed-completion-report.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
} from "./config.mjs";
|
||||
import { createManagedCompletionReport } from "./completion-report.mjs";
|
||||
import {
|
||||
completeVerifierRun,
|
||||
createVerifierRun,
|
||||
} from "../../lib/verifier-run.mjs";
|
||||
|
||||
const verifierRun = createVerifierRun();
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-managed-conformance-"));
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
|
||||
import { inspectManagedArtifactBytes } from "../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../../dist/application/ManagedApplicationGraphService.js";
|
||||
import { traceApplicationFeatureEvidence } from "../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
|
||||
import { createEvidence } from "../../dist/domain/evidence.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../dist/application/InvestigationProviders.js";
|
||||
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
|
||||
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../../../dist/application/managed/ManagedApplicationGraphService.js";
|
||||
import { traceApplicationFeatureEvidence } from "../../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
|
||||
import {
|
||||
ensure,
|
||||
methodTokenRow,
|
||||
parseManagedAppManifest,
|
||||
} from "./managed-conformance-manifest-parser.mjs";
|
||||
} from "./manifest-parser.mjs";
|
||||
|
||||
export const createManagedManifestVerifier = (context) => ({
|
||||
verifyManagedAppManifest: (rawManifest, manifestPath) =>
|
||||
|
|
@ -5,12 +5,12 @@ import { basename, isAbsolute, join, resolve } from "node:path";
|
|||
import assert from "node:assert/strict";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { inspectManagedArtifactBytes } from "../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { importManagedReconstructionEvidence } from "../../dist/application/ManagedReconstructionService.js";
|
||||
import { createEvidence } from "../../dist/domain/evidence.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../dist/application/InvestigationProviders.js";
|
||||
import { createManagedManifestVerifier } from "./managed-conformance-manifest.mjs";
|
||||
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { importManagedReconstructionEvidence } from "../../../dist/application/managed/ManagedReconstructionService.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
|
||||
import { createManagedManifestVerifier } from "./manifest.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
|
|
@ -2,9 +2,9 @@ import { createHash } from "node:crypto";
|
|||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
|
||||
import { buildManagedPeFixture } from "./managed-pe-fixture.mjs";
|
||||
import { createManagedConformanceOracleSupport } from "./managed-conformance-oracles.mjs";
|
||||
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
|
||||
import { buildManagedPeFixture } from "../../fixtures/managed/pe.mjs";
|
||||
import { createManagedConformanceOracleSupport } from "./oracles.mjs";
|
||||
|
||||
export const createManagedConformanceSupport = (context) => {
|
||||
const fixtureBytes = async (name, bytes) => {
|
||||
|
|
@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
|
|||
metadata:
|
||||
version: "28"
|
||||
tool_count: 131
|
||||
catalog_digest: "d18386ad8f46e7035cfa49f075c42e5c143f5b3f05c9459eb2d1c23c11112871"
|
||||
catalog_digest: "ab7efdea56f2e319bafbf1e1fc7d8ff998f3da1845968acb1614fc5be4dd6250"
|
||||
---
|
||||
|
||||
# REA
|
||||
|
|
|
|||
|
|
@ -32,7 +32,8 @@ export class JadxMcpTransport implements Transport {
|
|||
readonly runId = randomUUID();
|
||||
#spawned: SpawnedOwnedProviderProcess | undefined;
|
||||
#supervisor: ProviderProcessSupervisor | undefined;
|
||||
#buffer = Buffer.alloc(0);
|
||||
#frameBuffer = Buffer.alloc(0);
|
||||
#frameLength = 0;
|
||||
#bytes = 0;
|
||||
#started = false;
|
||||
#closed = false;
|
||||
|
|
@ -61,12 +62,16 @@ export class JadxMcpTransport implements Transport {
|
|||
stdin: "pipe",
|
||||
});
|
||||
this.#spawned = spawned;
|
||||
this.#supervisor = new ProviderProcessSupervisor({
|
||||
...spawned,
|
||||
ownsProcessLifetime: true,
|
||||
cleanup:
|
||||
spawned.cleanup ?? (() => cleanupOwnedProcessGroup(spawned.ownership)),
|
||||
});
|
||||
this.#supervisor = new ProviderProcessSupervisor(
|
||||
{
|
||||
...spawned,
|
||||
ownsProcessLifetime: true,
|
||||
cleanup:
|
||||
spawned.cleanup ??
|
||||
(() => cleanupOwnedProcessGroup(spawned.ownership)),
|
||||
},
|
||||
{ captureStdout: false },
|
||||
);
|
||||
if (
|
||||
spawned.process.stdin === undefined ||
|
||||
spawned.process.stdin === null ||
|
||||
|
|
@ -117,6 +122,8 @@ export class JadxMcpTransport implements Transport {
|
|||
|
||||
async #stop(): Promise<void> {
|
||||
this.#closed = true;
|
||||
this.#frameBuffer = Buffer.alloc(0);
|
||||
this.#frameLength = 0;
|
||||
// A cancellation may race acquisition. Join it before inspecting ownership.
|
||||
await this.#starting?.catch(() => undefined);
|
||||
const process = this.#spawned?.process;
|
||||
|
|
@ -161,22 +168,42 @@ export class JadxMcpTransport implements Transport {
|
|||
);
|
||||
return;
|
||||
}
|
||||
this.#buffer = Buffer.concat([this.#buffer, bytes]);
|
||||
try {
|
||||
let newline: number;
|
||||
while ((newline = this.#buffer.indexOf(10)) !== -1) {
|
||||
if (newline > MAX_FRAME_BYTES)
|
||||
let offset = 0;
|
||||
while (offset < bytes.length) {
|
||||
const newline = bytes.indexOf(10, offset);
|
||||
const end = newline === -1 ? bytes.length : newline;
|
||||
const fragment = bytes.subarray(offset, end);
|
||||
const frameBytes = this.#frameLength + fragment.length;
|
||||
if (frameBytes > MAX_FRAME_BYTES)
|
||||
throw new Error(`JADX MCP frame exceeds ${MAX_FRAME_BYTES} bytes`);
|
||||
const line = this.#buffer.subarray(0, newline).toString("utf8");
|
||||
this.#buffer = this.#buffer.subarray(newline + 1);
|
||||
this.#appendFrame(fragment, frameBytes);
|
||||
if (newline === -1) return;
|
||||
const line = this.#frameBuffer
|
||||
.subarray(0, this.#frameLength)
|
||||
.toString("utf8");
|
||||
this.#frameBuffer = Buffer.alloc(0);
|
||||
this.#frameLength = 0;
|
||||
this.onmessage?.(deserializeMessage(line));
|
||||
offset = newline + 1;
|
||||
}
|
||||
if (this.#buffer.length > MAX_FRAME_BYTES)
|
||||
throw new Error(`JADX MCP frame exceeds ${MAX_FRAME_BYTES} bytes`);
|
||||
} catch (cause) {
|
||||
this.#fail(cause instanceof Error ? cause : new Error(String(cause)));
|
||||
}
|
||||
};
|
||||
#appendFrame(fragment: Buffer, frameBytes: number): void {
|
||||
if (frameBytes > this.#frameBuffer.length) {
|
||||
const capacity = Math.min(
|
||||
MAX_FRAME_BYTES,
|
||||
Math.max(frameBytes, this.#frameBuffer.length * 2, 1),
|
||||
);
|
||||
const grown = Buffer.allocUnsafe(capacity);
|
||||
this.#frameBuffer.copy(grown, 0, 0, this.#frameLength);
|
||||
this.#frameBuffer = grown;
|
||||
}
|
||||
fragment.copy(this.#frameBuffer, this.#frameLength);
|
||||
this.#frameLength = frameBytes;
|
||||
}
|
||||
#fail(error: Error): void {
|
||||
this.#failure ??= error.message;
|
||||
this.onerror?.(error);
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import type { OfficialToolName } from "../contracts/officialToolContracts.js";
|
|||
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
|
||||
import type { NativeToolName } from "../contracts/nativeToolContracts.js";
|
||||
import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js";
|
||||
import type { ManagedToolName } from "../contracts/managedToolContracts.js";
|
||||
import type { ManagedToolName } from "../contracts/managed/managedToolContracts.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContrac
|
|||
import {
|
||||
isManagedToolName,
|
||||
type ManagedToolName,
|
||||
} from "../contracts/managedToolContracts.js";
|
||||
} from "../contracts/managed/managedToolContracts.js";
|
||||
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { type AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
|||
|
||||
import {
|
||||
installLinuxHopper,
|
||||
linuxHopperLauncherDigestSupported,
|
||||
linuxHopperInstallDisclosure,
|
||||
linuxPackageManagerCommands,
|
||||
linuxSharedLibrariesAvailable,
|
||||
|
|
@ -93,6 +94,20 @@ describe("Linux Hopper host classification", () => {
|
|||
});
|
||||
|
||||
describe("Linux Hopper installation", () => {
|
||||
it.each([
|
||||
[
|
||||
"Debian and Arch",
|
||||
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded",
|
||||
],
|
||||
["RPM", "1339f9e58377442b0c6fcb0dfc3cec20d593cc557408521fad9a00dbc6b8da13"],
|
||||
])("accepts the pinned %s launcher build", (_, digest) => {
|
||||
expect(linuxHopperLauncherDigestSupported(digest)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects an unverified launcher build", () => {
|
||||
expect(linuxHopperLauncherDigestSupported("0".repeat(64))).toBe(false);
|
||||
});
|
||||
|
||||
it("discloses the exact download, integrity evidence, and privileged command", () => {
|
||||
expect(linuxHopperInstallDisclosure("deb", false)).toEqual({
|
||||
downloadUrl:
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ import { execFileOutput } from "../process/ExecFileOutput.js";
|
|||
const execFileAsync = promisify(execFile);
|
||||
const DOWNLOAD_PREFIX = "https://www.hopperapp.com:443/downloader/public/";
|
||||
const MAX_PACKAGE_BYTES = 100_000_000;
|
||||
const SUPPORTED_HOPPER_SHA256 =
|
||||
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded";
|
||||
const SUPPORTED_HOPPER_SHA256 = new Set([
|
||||
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded",
|
||||
"1339f9e58377442b0c6fcb0dfc3cec20d593cc557408521fad9a00dbc6b8da13",
|
||||
]);
|
||||
interface LinuxHopperRelease {
|
||||
readonly filename: string;
|
||||
readonly file_length: string;
|
||||
|
|
@ -262,7 +264,7 @@ export const linuxHopperBinarySupported = async (
|
|||
try {
|
||||
const hash = createHash("sha256");
|
||||
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
||||
return hash.digest("hex") === SUPPORTED_HOPPER_SHA256;
|
||||
return linuxHopperLauncherDigestSupported(hash.digest("hex"));
|
||||
} catch (cause: unknown) {
|
||||
// best-effort cleanup: optional build probing; unreadable means unsupported.
|
||||
void cause;
|
||||
|
|
@ -270,6 +272,10 @@ export const linuxHopperBinarySupported = async (
|
|||
}
|
||||
};
|
||||
|
||||
/** Match a launcher extracted from one of the pinned official Linux packages. */
|
||||
export const linuxHopperLauncherDigestSupported = (digest: string): boolean =>
|
||||
SUPPORTED_HOPPER_SHA256.has(digest);
|
||||
|
||||
/** Interpret ldd output conservatively so a present but broken launcher is unhealthy. */
|
||||
export const linuxSharedLibrariesAvailable = (output: string): boolean =>
|
||||
!/(?:=>\s+not found|error while loading shared libraries)/iu.test(output);
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@ export class FakeSetupHost implements SetupHost {
|
|||
linuxDistribution = (): Promise<LinuxDistribution | undefined> =>
|
||||
Promise.resolve(this.distribution);
|
||||
hopperPath = (): Promise<string | undefined> => Promise.resolve(this.hopper);
|
||||
initialSetupState = async (scope?: DoctorScope) => ({
|
||||
...(this.hopper === undefined ? {} : { hopperPath: this.hopper }),
|
||||
providerEnvironment: await this.providerEnvironment(),
|
||||
doctor: await this.doctor(scope),
|
||||
});
|
||||
providerEnvironment = (): Promise<SetupProviderEnvironment> =>
|
||||
Promise.resolve({
|
||||
...(this.hopper === undefined
|
||||
|
|
|
|||
|
|
@ -0,0 +1,51 @@
|
|||
import { expect, it } from "vitest";
|
||||
|
||||
import { runSetup } from "./Setup.js";
|
||||
import { FakeSetupHost, options } from "./Setup.fixture.js";
|
||||
|
||||
it("uses the shared initial host snapshot for setup planning", async () => {
|
||||
const host = new FakeSetupHost();
|
||||
host.hopperPath = () => {
|
||||
throw new Error("setup snapshot should provide the Hopper path");
|
||||
};
|
||||
host.providerEnvironment = () => {
|
||||
throw new Error("setup snapshot should provide provider settings");
|
||||
};
|
||||
host.initialSetupState = async (scope) => ({
|
||||
hopperPath: "/snapshot/Hopper",
|
||||
providerEnvironment: { HOPPER_LAUNCHER_PATH: "/snapshot/Hopper" },
|
||||
doctor: await host.doctor(scope),
|
||||
});
|
||||
host.clients = [{ name: "codex", configPath: "/codex.json" }];
|
||||
|
||||
const result = await runSetup(
|
||||
{ ...options(false), dryRun: true, clientIds: ["codex"] },
|
||||
host,
|
||||
);
|
||||
|
||||
expect(result.status).toBe("planned");
|
||||
expect(result.plannedActions.map(({ id }) => id)).toContain(
|
||||
"configure_client:codex",
|
||||
);
|
||||
expect(host.doctorCalls).toBe(1);
|
||||
});
|
||||
|
||||
it("refreshes doctor after approved setup changes", async () => {
|
||||
const host = new FakeSetupHost();
|
||||
host.doctorHealthy = false;
|
||||
host.skill = "unchanged";
|
||||
host.clients = [{ name: "codex", configPath: "/codex.json" }];
|
||||
const configureClient = host.configureClient;
|
||||
host.configureClient = (client, providerEnvironment, command) => {
|
||||
host.doctorHealthy = true;
|
||||
return configureClient(client, providerEnvironment, command);
|
||||
};
|
||||
|
||||
const result = await runSetup(
|
||||
{ ...options(true), clientIds: ["codex"], installSkill: false },
|
||||
host,
|
||||
);
|
||||
|
||||
expect(result.doctor.healthy).toBe(true);
|
||||
expect(host.doctorCalls).toBe(2);
|
||||
});
|
||||
|
|
@ -42,8 +42,15 @@ export const runSetup = async (
|
|||
);
|
||||
const unsupported = await hostRemediation(host, false);
|
||||
if (unsupported !== undefined) return fail(unsupported);
|
||||
let hopperPath = await host.hopperPath();
|
||||
let providerEnvironment = await initialProviderEnvironment(host, hopperPath);
|
||||
const initialState = await host.initialSetupState?.(options.readinessScope);
|
||||
let hopperPath =
|
||||
initialState === undefined
|
||||
? await host.hopperPath()
|
||||
: initialState.hopperPath;
|
||||
let providerEnvironment =
|
||||
initialState === undefined
|
||||
? await initialProviderEnvironment(host, hopperPath)
|
||||
: initialState.providerEnvironment;
|
||||
const discovery = await discoverSetupState({
|
||||
host,
|
||||
providerEnvironment,
|
||||
|
|
@ -51,6 +58,9 @@ export const runSetup = async (
|
|||
proposeHopper:
|
||||
options.proposeHopper ?? (confirm !== undefined && !options.structured),
|
||||
doctorScope: options.readinessScope,
|
||||
...(initialState === undefined
|
||||
? {}
|
||||
: { initialDoctor: initialState.doctor }),
|
||||
});
|
||||
clientStates = discovery.clientStates;
|
||||
const clientSelectionAllowed =
|
||||
|
|
|
|||
|
|
@ -22,7 +22,12 @@ import {
|
|||
} from "./SetupClientConfiguration.js";
|
||||
import { setupInstallFailure } from "./SetupInstallFailure.js";
|
||||
import { providerRegistrationEnvironment } from "./SetupRegistrationEnvironment.js";
|
||||
import type { SetupHost, SetupProviderEnvironment } from "./SetupTypes.js";
|
||||
import type {
|
||||
SetupHost,
|
||||
SetupInitialState,
|
||||
SetupProviderEnvironment,
|
||||
} from "./SetupTypes.js";
|
||||
import type { DoctorScope } from "./Doctor.js";
|
||||
|
||||
/** Resolve the executable and arguments used in a managed MCP registration. */
|
||||
export const setupRegistrationCommand = (
|
||||
|
|
@ -91,6 +96,25 @@ export const systemSetupHost = (
|
|||
macosVersion: () => doctorHost.macosVersion(),
|
||||
linuxDistribution: readLinuxDistribution,
|
||||
hopperPath: async () => (await runDoctor(undefined, doctorHost)).hopperPath,
|
||||
initialSetupState: async (
|
||||
scope?: DoctorScope,
|
||||
): Promise<SetupInitialState> => {
|
||||
const diagnosis = await runDoctor(undefined, doctorHost, scope);
|
||||
return {
|
||||
...(diagnosis.hopperPath === undefined
|
||||
? {}
|
||||
: { hopperPath: diagnosis.hopperPath }),
|
||||
providerEnvironment: {
|
||||
...providerRegistrationEnvironment(
|
||||
diagnosis.providerInspections ?? [],
|
||||
),
|
||||
...(diagnosis.hopperPath === undefined
|
||||
? {}
|
||||
: { HOPPER_LAUNCHER_PATH: diagnosis.hopperPath }),
|
||||
},
|
||||
doctor: diagnosis,
|
||||
};
|
||||
},
|
||||
providerEnvironment: async () => {
|
||||
const diagnosis = await runDoctor(undefined, doctorHost);
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import type {
|
|||
} from "./SetupTypes.js";
|
||||
import type { SetupClient } from "./SupportedClients.js";
|
||||
import type { DoctorScope } from "./Doctor.js";
|
||||
import type { DoctorReport } from "./Doctor.js";
|
||||
|
||||
/** Read-only setup facts shared by client selection and preflight. */
|
||||
export interface SetupDiscovery {
|
||||
|
|
@ -36,8 +37,10 @@ export const discoverSetupState = async (input: {
|
|||
readonly forceHopperInstall: boolean;
|
||||
readonly proposeHopper: boolean;
|
||||
readonly doctorScope: DoctorScope | undefined;
|
||||
readonly initialDoctor?: DoctorReport;
|
||||
}): Promise<SetupDiscovery> => {
|
||||
const initialDoctor = await input.host.doctor(input.doctorScope);
|
||||
const initialDoctor =
|
||||
input.initialDoctor ?? (await input.host.doctor(input.doctorScope));
|
||||
const linuxHopperRepairNeeded = initialDoctor.checks.some(
|
||||
({ name, ok, detail }) =>
|
||||
!ok &&
|
||||
|
|
|
|||
|
|
@ -9,6 +9,13 @@ import type {
|
|||
/** Exact non-secret provider variables propagated into managed registrations. */
|
||||
export type SetupProviderEnvironment = Readonly<Record<string, string>>;
|
||||
|
||||
/** Read-only host facts shared by the initial setup plan. */
|
||||
export interface SetupInitialState {
|
||||
readonly hopperPath?: string;
|
||||
readonly providerEnvironment: SetupProviderEnvironment;
|
||||
readonly doctor: DoctorReport;
|
||||
}
|
||||
|
||||
/** Result of one backup/write/readback transaction. */
|
||||
export type ClientConfigurationResult =
|
||||
| {
|
||||
|
|
@ -38,6 +45,7 @@ export interface SetupHost {
|
|||
macosVersion(): Promise<string | undefined>;
|
||||
linuxDistribution(): Promise<LinuxDistribution | undefined>;
|
||||
hopperPath(): Promise<string | undefined>;
|
||||
initialSetupState?(scope?: DoctorScope): Promise<SetupInitialState>;
|
||||
installHopper(replaceExisting: boolean): Promise<SetupHopperInstallResult>;
|
||||
providerEnvironment?(): Promise<SetupProviderEnvironment>;
|
||||
detectedClients(): Promise<readonly SetupClient[]>;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
import type { JavaScriptApplicationGraph } from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import type { JavaScriptSemanticGraph } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import { javascriptRuntimeReconciliationResultSchema } from "../../domain/javascript/javascriptRuntimeReconciliationSchemas.js";
|
||||
import { managedApplicationGraphResultSchema } from "../../domain/managedApplicationGraph.js";
|
||||
import { managedApplicationGraphResultSchema } from "../../domain/managed/managedApplicationGraph.js";
|
||||
|
||||
/** Supported immutable source for an application-level graph workflow. */
|
||||
export interface ApplicationGraphEvidenceSource {
|
||||
|
|
|
|||
|
|
@ -3,19 +3,19 @@ import { z } from "zod";
|
|||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisProtocolError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { type AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
import {
|
||||
managedApplicationGraphResultSchema,
|
||||
projectManagedApplicationGraph,
|
||||
projectManagedApplicationGraphInputSchema,
|
||||
type ManagedApplicationGraphResult,
|
||||
type ProjectManagedApplicationGraphInput,
|
||||
} from "../domain/managedApplicationGraph.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { MANAGED_WORKFLOW_PROVIDER } from "./InvestigationProviders.js";
|
||||
} from "../../domain/managed/managedApplicationGraph.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { MANAGED_WORKFLOW_PROVIDER } from "../InvestigationProviders.js";
|
||||
|
||||
const OPERATION = "project_managed_application_graph" as const;
|
||||
|
||||
|
|
@ -8,22 +8,22 @@ import {
|
|||
managedMemberComparisonResultSchema,
|
||||
parseManagedMemberEvidence,
|
||||
type CompareManagedMembersInput,
|
||||
} from "../domain/managedMemberComparison.js";
|
||||
} from "../../domain/managed/managedMemberComparison.js";
|
||||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisProtocolError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
|
||||
import { type AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { parseBinaryTarget } from "./BinaryTargetResolver.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { inspectManagedMembersBytes } from "../dotnet/ManagedMemberInspector.js";
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
import { parseBinaryTarget } from "../BinaryTargetResolver.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { inspectManagedMembersBytes } from "../../dotnet/ManagedMemberInspector.js";
|
||||
import {
|
||||
MANAGED_STATIC_PROVIDER,
|
||||
MANAGED_WORKFLOW_PROVIDER,
|
||||
} from "./InvestigationProviders.js";
|
||||
} from "../InvestigationProviders.js";
|
||||
|
||||
/** Compare managed members from input parsed by a trusted adapter. */
|
||||
export const compareManagedMembersEvidenceValidated = (
|
||||
|
|
@ -3,18 +3,18 @@ import { z } from "zod";
|
|||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisProtocolError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { type AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
import {
|
||||
managedNativeVerificationInputSchema,
|
||||
verifyManagedNativeBoundaries,
|
||||
type ManagedNativeVerificationInput,
|
||||
type ManagedNativeVerificationResult,
|
||||
} from "../domain/managedNativeVerification.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { MANAGED_WORKFLOW_PROVIDER } from "./InvestigationProviders.js";
|
||||
} from "../../domain/managed/managedNativeVerification.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { MANAGED_WORKFLOW_PROVIDER } from "../InvestigationProviders.js";
|
||||
|
||||
const OPERATION = "verify_managed_native_boundaries" as const;
|
||||
|
||||
|
|
@ -2,13 +2,13 @@ import { createHash } from "node:crypto";
|
|||
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE } from "../contracts/managedWorkflowExamples.js";
|
||||
import { MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE } from "../../contracts/managed/managedWorkflowExamples.js";
|
||||
import {
|
||||
importManagedReconstruction,
|
||||
managedReconstructionImportInputSchema,
|
||||
} from "../domain/managedReconstruction.js";
|
||||
import { managedMemberInspectionSchema } from "../domain/managedArtifact.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
} from "../../domain/managed/managedReconstruction.js";
|
||||
import { managedMemberInspectionSchema } from "../../domain/managed/managedArtifact.js";
|
||||
import { createEvidence } from "../../domain/evidence.js";
|
||||
import { importManagedReconstructionEvidence } from "./ManagedReconstructionService.js";
|
||||
|
||||
const exampleInput = () =>
|
||||
|
|
@ -3,18 +3,18 @@ import { z } from "zod";
|
|||
import {
|
||||
AnalysisInputError,
|
||||
AnalysisProtocolError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import { type AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
} from "../../domain/analysisErrorCore.js";
|
||||
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import { jsonValueSchema } from "../../domain/jsonValue.js";
|
||||
import {
|
||||
importManagedReconstruction,
|
||||
managedReconstructionImportInputSchema,
|
||||
type ManagedReconstructionImportInput,
|
||||
type ManagedReconstructionImportResult,
|
||||
} from "../domain/managedReconstruction.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { MANAGED_WORKFLOW_PROVIDER } from "./InvestigationProviders.js";
|
||||
} from "../../domain/managed/managedReconstruction.js";
|
||||
import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { MANAGED_WORKFLOW_PROVIDER } from "../InvestigationProviders.js";
|
||||
|
||||
const OPERATION = "import_managed_reconstruction" as const;
|
||||
|
||||
|
|
@ -143,14 +143,29 @@ const asarFailure = (
|
|||
path: string,
|
||||
operation: string,
|
||||
cause: unknown,
|
||||
): ArtifactReaderFailure =>
|
||||
cause instanceof ArtifactReaderFailure
|
||||
? cause
|
||||
: new ArtifactReaderFailure(
|
||||
"format",
|
||||
`Malformed or unreadable ASAR during ${operation}: ${path}`,
|
||||
{ cause },
|
||||
);
|
||||
): ArtifactReaderFailure => {
|
||||
if (cause instanceof ArtifactReaderFailure) return cause;
|
||||
if (isFilesystemFailure(cause))
|
||||
return new ArtifactReaderFailure(
|
||||
"io",
|
||||
`Could not ${operation} ASAR at ${path}: ${cause.message}`,
|
||||
{ cause },
|
||||
);
|
||||
return new ArtifactReaderFailure(
|
||||
"format",
|
||||
`Malformed ASAR during ${operation}: ${path}`,
|
||||
{ cause },
|
||||
);
|
||||
};
|
||||
|
||||
const isFilesystemFailure = (
|
||||
cause: unknown,
|
||||
): cause is NodeJS.ErrnoException & Error =>
|
||||
cause instanceof Error &&
|
||||
"errno" in cause &&
|
||||
typeof cause.errno === "number" &&
|
||||
"syscall" in cause &&
|
||||
typeof cause.syscall === "string";
|
||||
|
||||
const isMissingFile = (cause: unknown): boolean =>
|
||||
typeof cause === "object" &&
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ export class DirectoryArtifactReader implements ArtifactReader {
|
|||
);
|
||||
const handle = await open(
|
||||
entry.adapterKey,
|
||||
constants.O_RDONLY | constants.O_NOFOLLOW,
|
||||
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
||||
);
|
||||
try {
|
||||
const observed = await handle.stat();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { z } from "incur";
|
||||
|
||||
import { compareManagedMemberPaths } from "../application/ManagedMemberComparisonService.js";
|
||||
import { verifyManagedNativeBoundariesEvidence } from "../application/ManagedNativeVerificationService.js";
|
||||
import { importManagedReconstructionEvidence } from "../application/ManagedReconstructionService.js";
|
||||
import { compareManagedMemberPaths } from "../application/managed/ManagedMemberComparisonService.js";
|
||||
import { verifyManagedNativeBoundariesEvidence } from "../application/managed/ManagedNativeVerificationService.js";
|
||||
import { importManagedReconstructionEvidence } from "../application/managed/ManagedReconstructionService.js";
|
||||
import { runProviderAnalysis } from "../application/DirectAnalysis.js";
|
||||
import { parseCliJsonInput } from "../cliJsonInput.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from "incur";
|
||||
|
||||
import { projectManagedApplicationGraphEvidence } from "../application/ManagedApplicationGraphService.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../application/managed/ManagedApplicationGraphService.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { parseCliJsonInput } from "../cliJsonInput.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@ import { NATIVE_TOOL_CONTRACTS } from "./nativeToolContracts.js";
|
|||
import { ARTIFACT_TOOL_CONTRACTS } from "./artifactToolContracts.js";
|
||||
import { FIRMWARE_TOOL_CONTRACTS } from "./firmware/firmwareToolContracts.js";
|
||||
import { ANDROID_TOOL_CONTRACTS } from "./android/androidToolContracts.js";
|
||||
import { MANAGED_TOOL_CONTRACTS } from "./managedToolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managedWorkflowToolContracts.js";
|
||||
import { MANAGED_TOOL_CONTRACTS } from "./managed/managedToolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managed/managedWorkflowToolContracts.js";
|
||||
import { BROWSER_TOOL_CONTRACTS } from "./browserToolContracts.js";
|
||||
import { BROWSER_SCENARIO_TOOL_CONTRACTS } from "./browserScenarioToolContracts.js";
|
||||
import { ELECTRON_TOOL_CONTRACTS } from "./javascript/electronToolContracts.js";
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import type { ToolContract } from "./toolContracts.js";
|
||||
import { managedOutputSchemas } from "./toolOutputSchemas.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import { requireOutputSchema } from "./toolOutputSchemaPrimitives.js";
|
||||
import type { ToolContract } from "../toolContracts.js";
|
||||
import { managedOutputSchemas } from "../toolOutputSchemas.js";
|
||||
import { toolContractMetadata } from "../toolEffects.js";
|
||||
import { requireOutputSchema } from "../toolOutputSchemaPrimitives.js";
|
||||
|
||||
/** Exact caller boundary for execution-free PE/CLI triage and identity. */
|
||||
const managedTargetInputSchema = z.object({
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { createEvidence } from "../../domain/evidence.js";
|
||||
import type {
|
||||
ManagedMemberInspection,
|
||||
ManagedNativeBoundaryInspection,
|
||||
} from "../domain/managedArtifact.js";
|
||||
import type { InspectMacho } from "../domain/nativeInspection.js";
|
||||
} from "../../domain/managed/managedArtifact.js";
|
||||
import type { InspectMacho } from "../../domain/nativeInspection.js";
|
||||
|
||||
const MANAGED_STATIC_EXAMPLE_PROVIDER = {
|
||||
id: "rea-dotnet-static",
|
||||
|
|
@ -1,17 +1,17 @@
|
|||
import { z } from "zod";
|
||||
import { evidenceSchema } from "../domain/evidence.js";
|
||||
import { evidenceSchema } from "../../domain/evidence.js";
|
||||
|
||||
import { managedReconstructionImportInputSchema } from "../domain/managedReconstruction.js";
|
||||
import type { ToolContract } from "./toolContracts.js";
|
||||
import { managedWorkflowOutputSchemas } from "./toolOutputSchemas.js";
|
||||
import { managedReconstructionImportInputSchema } from "../../domain/managed/managedReconstruction.js";
|
||||
import type { ToolContract } from "../toolContracts.js";
|
||||
import { managedWorkflowOutputSchemas } from "../toolOutputSchemas.js";
|
||||
import {
|
||||
MANAGED_MEMBER_COMPARISON_EXAMPLE,
|
||||
MANAGED_NATIVE_VERIFICATION_EXAMPLE,
|
||||
MANAGED_APPLICATION_GRAPH_EXAMPLE,
|
||||
MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE,
|
||||
} from "./managedWorkflowExamples.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import { requireOutputSchema } from "./toolOutputSchemaPrimitives.js";
|
||||
import { toolContractMetadata } from "../toolEffects.js";
|
||||
import { requireOutputSchema } from "../toolOutputSchemaPrimitives.js";
|
||||
|
||||
/** Inputs for comparing two managed observations carried inline. */
|
||||
export const compareManagedMembersReferenceInputSchema = z
|
||||
|
|
@ -80,7 +80,7 @@ export const SESSION_TOOL_CONTRACTS = [
|
|||
),
|
||||
session(
|
||||
"capture_process_scenario",
|
||||
"Run one caller-selected command under a PTY and return process capture Evidence with residual unknowns. A command name resolves through the inherited PATH; the working directory defaults to the caller's current directory; host environment variables are inherited with scenario overrides. Filesystem snapshots are opt-in through filesystem_observation_paths. The target runs with the current user's permissions; this is not a security sandbox.",
|
||||
"Run one caller-selected command under a PTY and return process capture Evidence with residual unknowns. A command name resolves through the inherited PATH; the working directory defaults to the caller's current directory; host environment variables are inherited with scenario overrides. Environment keys cannot contain '=' or NUL, and REA_PROCESS_RUN_ID is reserved for process ownership. Filesystem snapshots are opt-in through filesystem_observation_paths. The target runs with the current user's permissions; this is not a security sandbox.",
|
||||
processScenarioSchema,
|
||||
),
|
||||
session(
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ import { OFFICIAL_TOOL_CONTRACTS } from "./officialToolContracts.js";
|
|||
import { SESSION_TOOL_CONTRACTS } from "./sessionToolContracts.js";
|
||||
import { TOOL_CONTRACTS } from "./toolContracts.js";
|
||||
import { ARTIFACT_TOOL_CONTRACTS } from "./artifactToolContracts.js";
|
||||
import { MANAGED_TOOL_CONTRACTS } from "./managedToolContracts.js";
|
||||
import { MANAGED_TOOL_CONTRACTS } from "./managed/managedToolContracts.js";
|
||||
import { FIRMWARE_TOOL_CONTRACTS } from "./firmware/firmwareToolContracts.js";
|
||||
import { ANDROID_TOOL_CONTRACTS } from "./android/androidToolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managedWorkflowToolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managed/managedWorkflowToolContracts.js";
|
||||
import { NATIVE_TOOL_CONTRACTS } from "./nativeToolContracts.js";
|
||||
import { BROWSER_PROVIDER_TOOL_CONTRACTS } from "./browserProviderToolContracts.js";
|
||||
import { ELECTRON_TOOL_CONTRACTS } from "./javascript/electronToolContracts.js";
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { NATIVE_TOOL_CONTRACTS } from "./nativeToolContracts.js";
|
||||
import { ARTIFACT_TOOL_CONTRACTS } from "./artifactToolContracts.js";
|
||||
import { MANAGED_TOOL_CONTRACTS } from "./managedToolContracts.js";
|
||||
import { MANAGED_TOOL_CONTRACTS } from "./managed/managedToolContracts.js";
|
||||
import { FIRMWARE_TOOL_CONTRACTS } from "./firmware/firmwareToolContracts.js";
|
||||
import { ANDROID_TOOL_CONTRACTS } from "./android/androidToolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managedWorkflowToolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managed/managedWorkflowToolContracts.js";
|
||||
import { BROWSER_PROVIDER_TOOL_CONTRACTS } from "./browserProviderToolContracts.js";
|
||||
import { ELECTRON_TOOL_CONTRACTS } from "./javascript/electronToolContracts.js";
|
||||
import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "./javascript/javascriptRuntimeObservationToolContracts.js";
|
||||
|
|
|
|||
|
|
@ -37,11 +37,11 @@ import {
|
|||
managedArtifactInspectionSchema,
|
||||
managedMemberInspectionSchema,
|
||||
managedNativeBoundaryInspectionSchema,
|
||||
} from "../domain/managedArtifact.js";
|
||||
import { managedMemberComparisonResultSchema } from "../domain/managedMemberComparison.js";
|
||||
import { managedNativeVerificationResultSchema } from "../domain/managedNativeVerification.js";
|
||||
import { managedReconstructionImportResultSchema } from "../domain/managedReconstruction.js";
|
||||
import { managedApplicationGraphResultSchema } from "../domain/managedApplicationGraph.js";
|
||||
} from "../domain/managed/managedArtifact.js";
|
||||
import { managedMemberComparisonResultSchema } from "../domain/managed/managedMemberComparison.js";
|
||||
import { managedNativeVerificationResultSchema } from "../domain/managed/managedNativeVerification.js";
|
||||
import { managedReconstructionImportResultSchema } from "../domain/managed/managedReconstruction.js";
|
||||
import { managedApplicationGraphResultSchema } from "../domain/managed/managedApplicationGraph.js";
|
||||
import { artifactComparisonResultSchema } from "../domain/artifactComparison.js";
|
||||
import { functionComparisonResultSchema } from "../domain/functionComparison.js";
|
||||
import { bundleComparisonResultSchema } from "../domain/bundleComparison.js";
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ import {
|
|||
MANAGED_APPLICATION_GRAPH_EXAMPLE,
|
||||
MANAGED_MEMBER_COMPARISON_EXAMPLE,
|
||||
MANAGED_NATIVE_VERIFICATION_EXAMPLE,
|
||||
} from "./managedWorkflowExamples.js";
|
||||
} from "./managed/managedWorkflowExamples.js";
|
||||
import {
|
||||
compareManagedMembersReferenceInputSchema,
|
||||
managedApplicationGraphReferenceInputSchema,
|
||||
managedNativeVerificationReferenceInputSchema,
|
||||
} from "./managedWorkflowToolContracts.js";
|
||||
} from "./managed/managedWorkflowToolContracts.js";
|
||||
|
||||
describe("workflow input contracts", () => {
|
||||
it("rejects application Evidence ID references and duplicate inline records", () => {
|
||||
|
|
|
|||
|
|
@ -309,6 +309,29 @@ describe("Apple bundle layouts and containers", () => {
|
|||
expect(
|
||||
result.bundles.find(({ path }) => path === framework)?.info_plist_path,
|
||||
).toBeNull();
|
||||
// Files beside the only real version are not versions.
|
||||
const withFiles = project(
|
||||
inventoryEvidence("zip", "Fixture.zip", [
|
||||
macho("Fixture.app/Contents/MacOS/Fixture"),
|
||||
{ path: `${framework}/Versions/A/Resources/Info.plist` },
|
||||
{ path: `${framework}/Versions/.DS_Store` },
|
||||
{ path: `${framework}/Versions/Current`, kind: "symlink" },
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
withFiles.bundles.find(({ path }) => path === framework)?.info_plist_path,
|
||||
).toBe(`${framework}/Versions/A/Resources/Info.plist`);
|
||||
// Only A has a plist, but Versions/Current may select B.
|
||||
const onlyA = project(
|
||||
inventoryEvidence("zip", "Fixture.zip", [
|
||||
macho("Fixture.app/Contents/MacOS/Fixture"),
|
||||
{ path: `${framework}/Versions/A/Resources/Info.plist` },
|
||||
macho(`${framework}/Versions/B/Core`),
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
onlyA.bundles.find(({ path }) => path === framework)?.info_plist_path,
|
||||
).toBeNull();
|
||||
expect(result.limitations).toContainEqual(
|
||||
expect.stringContaining("Versions/Current selects is unknown"),
|
||||
);
|
||||
|
|
@ -438,3 +461,37 @@ describe("macOS application scope", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Apple application bridge candidates", () => {
|
||||
it("pairs scripts and native code only within one application root", () => {
|
||||
const result = project(
|
||||
inventoryEvidence("zip", "Suite.zip", [
|
||||
macho("First.app/Contents/MacOS/First"),
|
||||
{
|
||||
path: "First.app/Contents/Resources/first.js",
|
||||
format: "javascript-bundle",
|
||||
},
|
||||
macho("Second.app/Contents/MacOS/Second"),
|
||||
{
|
||||
path: "Second.app/Contents/Resources/second.js",
|
||||
format: "javascript-bundle",
|
||||
},
|
||||
]),
|
||||
);
|
||||
expect(result.application_roots).toEqual(["First.app", "Second.app"]);
|
||||
expect(
|
||||
result.bridge_candidates.map(
|
||||
({ source_path: source, native_path: target }) => [source, target],
|
||||
),
|
||||
).toEqual([
|
||||
[
|
||||
"First.app/Contents/Resources/first.js",
|
||||
"First.app/Contents/MacOS/First",
|
||||
],
|
||||
[
|
||||
"Second.app/Contents/Resources/second.js",
|
||||
"Second.app/Contents/MacOS/Second",
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ export const projectAppleApplication = (
|
|||
...classified.executables,
|
||||
...classified.native_libraries,
|
||||
]),
|
||||
roots,
|
||||
);
|
||||
const uninventoriedImage =
|
||||
rootFormat === "dmg" && inventory.occurrences.length === 1;
|
||||
|
|
@ -305,18 +306,27 @@ const identifyRuntimeFamilies = (all: readonly Component[]) => {
|
|||
return [...families].sort(compare);
|
||||
};
|
||||
|
||||
/** Pair scripts and native code only within the same application root. */
|
||||
const identifyBridgeCandidates = (
|
||||
scripts: readonly Component[],
|
||||
native: readonly Component[],
|
||||
) => ({
|
||||
candidates: scripts.flatMap((script) =>
|
||||
native.map((item) => ({
|
||||
source_path: script.path,
|
||||
native_path: item.path,
|
||||
basis: bridgeBasis(item.path),
|
||||
})),
|
||||
),
|
||||
});
|
||||
roots: readonly string[],
|
||||
) => {
|
||||
const owner = (path: string): string | null =>
|
||||
roots.find((root) => isWithin(path, root)) ?? null;
|
||||
return {
|
||||
candidates: scripts.flatMap((script) => {
|
||||
const root = owner(script.path);
|
||||
return native
|
||||
.filter(({ path }) => owner(path) === root)
|
||||
.map((item) => ({
|
||||
source_path: script.path,
|
||||
native_path: item.path,
|
||||
basis: bridgeBasis(item.path),
|
||||
}));
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
const bridgeBasis = (
|
||||
path: string,
|
||||
|
|
@ -360,7 +370,7 @@ const projectionLimitations = (facts: {
|
|||
layout === "versioned-framework" && plist === null,
|
||||
)
|
||||
? [
|
||||
"A versioned framework has zero or several Versions/*/Resources/Info.plist files; the one Versions/Current selects is unknown because symlink targets are not inventoried.",
|
||||
"A versioned framework has several version directories or no Versions/*/Resources/Info.plist; the Info.plist that Versions/Current selects is unknown because symlink targets are not inventoried.",
|
||||
]
|
||||
: []),
|
||||
...(facts.symlinks
|
||||
|
|
|
|||
|
|
@ -141,14 +141,18 @@ export const detectBundles = (
|
|||
interface Tree {
|
||||
readonly children: ReadonlyMap<string, ReadonlySet<string>>;
|
||||
readonly files: ReadonlyMap<string, Component>;
|
||||
/** Paths observed as directories, or implied by a deeper path. */
|
||||
readonly directories: ReadonlySet<string>;
|
||||
}
|
||||
|
||||
const indexTree = (entries: readonly Entry[]): Tree => {
|
||||
const children = new Map<string, Set<string>>();
|
||||
const files = new Map<string, Component>();
|
||||
const directories = new Set<string>();
|
||||
for (const { path, kind, component } of entries) {
|
||||
if (isSidecar(path)) continue;
|
||||
if (kind === "file" && component !== undefined) files.set(path, component);
|
||||
if (kind === "directory") directories.add(path);
|
||||
const segments = path.split("/");
|
||||
let parent = "";
|
||||
for (const segment of segments) {
|
||||
|
|
@ -156,9 +160,10 @@ const indexTree = (entries: readonly Entry[]): Tree => {
|
|||
names.add(segment);
|
||||
children.set(parent, names);
|
||||
parent = joinPath(parent, segment);
|
||||
if (parent !== path) directories.add(parent);
|
||||
}
|
||||
}
|
||||
return { children, files };
|
||||
return { children, files, directories };
|
||||
};
|
||||
|
||||
const joinPath = (directory: string, name: string): string =>
|
||||
|
|
@ -223,9 +228,12 @@ const contentDirectories = (
|
|||
if (layout === "macos-deep") return [joinPath(directory, "Contents")];
|
||||
if (layout === "shallow") return [directory];
|
||||
const versions = joinPath(directory, "Versions");
|
||||
// Only real version directories count: Current is a symlink, and files
|
||||
// such as .DS_Store are not versions.
|
||||
return [...(tree.children.get(versions) ?? [])]
|
||||
.filter((version) => version !== "Current")
|
||||
.map((version) => joinPath(versions, version))
|
||||
.filter((version) => tree.directories.has(version))
|
||||
.sort(compare);
|
||||
};
|
||||
|
||||
|
|
@ -234,7 +242,11 @@ const infoPlistPath = (
|
|||
layout: Bundle["layout"],
|
||||
tree: Tree,
|
||||
): string | null => {
|
||||
const candidates = contentDirectories(bundle, layout, tree)
|
||||
const directories = contentDirectories(bundle, layout, tree);
|
||||
// With several real versions, Versions/Current (a symlink outside the
|
||||
// inventory) decides which plist applies, so the plist stays unknown.
|
||||
if (directories.length !== 1) return null;
|
||||
const candidates = directories
|
||||
.map((directory) =>
|
||||
joinPath(
|
||||
directory,
|
||||
|
|
|
|||
|
|
@ -119,14 +119,30 @@ const addExport = (output: Set<string>, name: string): void => {
|
|||
};
|
||||
|
||||
const memberPath = (node: t.Node | null | undefined): string => {
|
||||
if (node === undefined || node === null) return "";
|
||||
if (t.isIdentifier(node) || t.isPrivateName(node))
|
||||
return t.isIdentifier(node) ? node.name : "";
|
||||
if (t.isThisExpression(node)) return "this";
|
||||
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
const object = t.isNode(node.object) ? memberPath(node.object) : "";
|
||||
const property = propertyName(node.property);
|
||||
return object === "" ? property : `${object}.${property}`;
|
||||
const properties: string[] = [];
|
||||
let current = node;
|
||||
while (
|
||||
current !== undefined &&
|
||||
current !== null &&
|
||||
(t.isMemberExpression(current) || t.isOptionalMemberExpression(current))
|
||||
) {
|
||||
properties.push(propertyName(current.property));
|
||||
current = t.isNode(current.object) ? current.object : undefined;
|
||||
}
|
||||
return "";
|
||||
|
||||
const root =
|
||||
current !== undefined && t.isIdentifier(current)
|
||||
? current.name
|
||||
: current !== undefined && t.isThisExpression(current)
|
||||
? "this"
|
||||
: "";
|
||||
if (properties.length === 0) return root;
|
||||
|
||||
properties.reverse();
|
||||
const path = root === "" ? [] : [root];
|
||||
for (const property of properties) {
|
||||
if (path.length === 0 && property === "") continue;
|
||||
path.push(property);
|
||||
}
|
||||
return path.join(".");
|
||||
};
|
||||
|
|
|
|||
|
|
@ -192,6 +192,51 @@ describe("JavaScript semantic analysis: calls 2", () => {
|
|||
).not.toBe(left.functionFingerprints[0]?.components.literalSetSha256);
|
||||
});
|
||||
|
||||
it("keeps fingerprint relations ordered and effects scoped to their owner", () => {
|
||||
const ordered = analyzeJavaScriptSemantics(`
|
||||
function first() {}
|
||||
function second(one, two) {}
|
||||
function ordered() { first(); second(1, 2); }
|
||||
function timerOwner() { setTimeout(work, 10); }
|
||||
function networkOwner() { fetch('/data'); }
|
||||
function noEffects() { return 1; }
|
||||
`);
|
||||
const reversed = analyzeJavaScriptSemantics(`
|
||||
function first() {}
|
||||
function second(one, two) {}
|
||||
function ordered() { second(1, 2); first(); }
|
||||
function timerOwner() { setTimeout(work, 10); }
|
||||
function networkOwner() { fetch('/data'); }
|
||||
function noEffects() { return 1; }
|
||||
`);
|
||||
const fingerprint = (ir: typeof ordered, name: string) =>
|
||||
ir.functionFingerprints.find((value) =>
|
||||
ir.callables.some(
|
||||
(callable) =>
|
||||
callable.callableId === value.callableId && callable.name === name,
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
fingerprint(ordered, "ordered")?.components.relationShapeSha256,
|
||||
).not.toBe(
|
||||
fingerprint(reversed, "ordered")?.components.relationShapeSha256,
|
||||
);
|
||||
expect(fingerprint(ordered, "timerOwner")?.components.effects).toContain(
|
||||
"timer",
|
||||
);
|
||||
expect(fingerprint(ordered, "networkOwner")?.components.effects).toContain(
|
||||
"network",
|
||||
);
|
||||
expect(fingerprint(ordered, "noEffects")?.components.effects).toEqual([]);
|
||||
expect(
|
||||
fingerprint(ordered, "timerOwner")?.components.effects,
|
||||
).not.toContain("network");
|
||||
expect(
|
||||
fingerprint(ordered, "networkOwner")?.components.effects,
|
||||
).not.toContain("timer");
|
||||
});
|
||||
|
||||
it("retains ambiguous local callees and explicit dynamic frontiers", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
function left(value) { return value; }
|
||||
|
|
|
|||
|
|
@ -149,6 +149,31 @@ describe("JavaScript semantic analysis: dataflow 1", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("JavaScript semantic call result flows", () => {
|
||||
it("links only directly assigned scalar call results", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
function run() {
|
||||
const direct = produce();
|
||||
const { field } = unpack();
|
||||
const nested = outer(inner());
|
||||
return direct;
|
||||
}
|
||||
`);
|
||||
const bindingNames = new Map(
|
||||
ir.bindings.map(({ bindingId, name }) => [bindingId, name]),
|
||||
);
|
||||
|
||||
expect(
|
||||
ir.callResultFlows.map(({ bindingId }) => bindingNames.get(bindingId)),
|
||||
).toEqual(["direct", "nested"]);
|
||||
expect(ir.callSites).toHaveLength(4);
|
||||
expect(ir.callResultFlows.map(({ callSiteId }) => callSiteId)).toEqual([
|
||||
ir.callSites[0]?.callSiteId,
|
||||
ir.callSites[2]?.callSiteId,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("JavaScript semantic analysis: read-modify-write", () => {
|
||||
it.each(["+= 1", "++", "--", "||= 1", "&&= 1", "??= 1"])(
|
||||
"retains the property read in a read-modify-write operation: %s",
|
||||
|
|
|
|||
|
|
@ -183,9 +183,7 @@ const collectCallResultFlow = (
|
|||
context.ancestors,
|
||||
context.state,
|
||||
).filter(({ projectionPath }) => projectionPath.length === 0)) {
|
||||
const binding = [...context.state.bindingsById.values()].find(
|
||||
({ bindingId }) => bindingId === assigned.bindingId,
|
||||
);
|
||||
const binding = context.state.bindingsById.get(assigned.bindingId);
|
||||
if (binding === undefined) continue;
|
||||
const identifierRange = range(assigned.identifier);
|
||||
const definition = binding.definitions.find(
|
||||
|
|
|
|||
|
|
@ -50,30 +50,36 @@ export const collectJavaScriptSemanticFingerprints = (
|
|||
resourceOperations,
|
||||
parserPartial,
|
||||
} = input;
|
||||
const callSitesByCaller = groupByOwner(
|
||||
calls.callSites,
|
||||
({ callerCallableId }) => callerCallableId,
|
||||
);
|
||||
const captureCountByCallable = countByOwner(
|
||||
calls.closureCaptures,
|
||||
({ callableId }) => callableId,
|
||||
);
|
||||
const promiseEffectsByOwner = groupByOwner(promises, ownerId);
|
||||
const eventEffectsByOwner = groupByOwner(events, ownerId);
|
||||
const timerEffectsByOwner = groupByOwner(timers, ownerId);
|
||||
const childProcessEffectsByOwner = groupByOwner(
|
||||
[...childProcessSpawns, ...childProcessInteractions],
|
||||
ownerId,
|
||||
);
|
||||
const networkEffectsByOwner = groupByOwner(requestOperations, ownerId);
|
||||
const resourceEffectsByOwner = groupByOwner(resourceOperations, ownerId);
|
||||
return callables.flatMap((callable) => {
|
||||
if (callable.kind === "class") return [];
|
||||
const node = state.callableNodesById.get(callable.callableId);
|
||||
if (node === undefined) return [];
|
||||
const syntax = fingerprintSyntax(node, callable.callableId);
|
||||
const promiseEffects = promises.filter(
|
||||
({ ownerCallableId }) => ownerCallableId === callable.callableId,
|
||||
);
|
||||
const eventEffects = events.filter(
|
||||
({ ownerCallableId }) => ownerCallableId === callable.callableId,
|
||||
);
|
||||
const timerEffects = timers.filter(
|
||||
({ ownerCallableId }) => ownerCallableId === callable.callableId,
|
||||
);
|
||||
const childProcessEffects = [
|
||||
...childProcessSpawns,
|
||||
...childProcessInteractions,
|
||||
].filter(({ ownerCallableId }) => ownerCallableId === callable.callableId);
|
||||
const networkEffects = requestOperations.filter(
|
||||
({ ownerCallableId }) => ownerCallableId === callable.callableId,
|
||||
);
|
||||
const resourceEffects = resourceOperations.filter(
|
||||
({ ownerCallableId }) => ownerCallableId === callable.callableId,
|
||||
);
|
||||
const promiseEffects = promiseEffectsByOwner.get(callable.callableId) ?? [];
|
||||
const eventEffects = eventEffectsByOwner.get(callable.callableId) ?? [];
|
||||
const timerEffects = timerEffectsByOwner.get(callable.callableId) ?? [];
|
||||
const childProcessEffects =
|
||||
childProcessEffectsByOwner.get(callable.callableId) ?? [];
|
||||
const networkEffects = networkEffectsByOwner.get(callable.callableId) ?? [];
|
||||
const resourceEffects =
|
||||
resourceEffectsByOwner.get(callable.callableId) ?? [];
|
||||
return [
|
||||
{
|
||||
callableId: callable.callableId,
|
||||
|
|
@ -86,7 +92,12 @@ export const collectJavaScriptSemanticFingerprints = (
|
|||
normalizedAstSha256: digest(syntax.normalizedTokens),
|
||||
controlFlowSha256: digest(syntax.controlFlowTokens),
|
||||
relationShapeSha256: digest(
|
||||
relationShape(callable, calls, promiseEffects),
|
||||
relationShape(
|
||||
callable,
|
||||
callSitesByCaller.get(callable.callableId) ?? [],
|
||||
captureCountByCallable.get(callable.callableId) ?? 0,
|
||||
promiseEffects,
|
||||
),
|
||||
),
|
||||
literalSetSha256: digest(
|
||||
[...new Set(syntax.literals)].sort(compareCodePoints),
|
||||
|
|
@ -114,6 +125,36 @@ export const collectJavaScriptSemanticFingerprints = (
|
|||
});
|
||||
};
|
||||
|
||||
const ownerId = <T extends { readonly ownerCallableId: string | null }>(
|
||||
value: T,
|
||||
): string | null => value.ownerCallableId;
|
||||
|
||||
const groupByOwner = <T, K extends string | null>(
|
||||
values: readonly T[],
|
||||
getOwner: (value: T) => K,
|
||||
): Map<K, T[]> => {
|
||||
const grouped = new Map<K, T[]>();
|
||||
for (const value of values) {
|
||||
const owner = getOwner(value);
|
||||
const ownedValues = grouped.get(owner);
|
||||
if (ownedValues === undefined) grouped.set(owner, [value]);
|
||||
else ownedValues.push(value);
|
||||
}
|
||||
return grouped;
|
||||
};
|
||||
|
||||
const countByOwner = <T, K extends string | null>(
|
||||
values: readonly T[],
|
||||
getOwner: (value: T) => K,
|
||||
): Map<K, number> => {
|
||||
const counts = new Map<K, number>();
|
||||
for (const value of values) {
|
||||
const owner = getOwner(value);
|
||||
counts.set(owner, (counts.get(owner) ?? 0) + 1);
|
||||
}
|
||||
return counts;
|
||||
};
|
||||
|
||||
interface FingerprintSyntax {
|
||||
readonly normalizedTokens: readonly string[];
|
||||
readonly controlFlowTokens: readonly string[];
|
||||
|
|
@ -229,21 +270,16 @@ const semanticLiteral = (node: t.Node): string | null => {
|
|||
|
||||
const relationShape = (
|
||||
callable: JavaScriptSemanticCallable,
|
||||
calls: JavaScriptSemanticCallAnalysis,
|
||||
callSites: JavaScriptSemanticCallAnalysis["callSites"],
|
||||
captureCount: number,
|
||||
promises: readonly JavaScriptSemanticPromiseOperation[],
|
||||
): string[] => [
|
||||
`returns:${String(callable.returnSites.length)}:${callable.returnCoverage.status}`,
|
||||
...calls.callSites
|
||||
.filter(({ callerCallableId }) => callerCallableId === callable.callableId)
|
||||
.map(
|
||||
({ kind, resolution, arguments: args, calleeCallableIds }) =>
|
||||
`call:${kind}:${resolution}:${String(args.length)}:${String(calleeCallableIds.length)}`,
|
||||
),
|
||||
`captures:${String(
|
||||
calls.closureCaptures.filter(
|
||||
({ callableId }) => callableId === callable.callableId,
|
||||
).length,
|
||||
)}`,
|
||||
...callSites.map(
|
||||
({ kind, resolution, arguments: args, calleeCallableIds }) =>
|
||||
`call:${kind}:${resolution}:${String(args.length)}:${String(calleeCallableIds.length)}`,
|
||||
),
|
||||
`captures:${String(captureCount)}`,
|
||||
...promises.map(
|
||||
({ kind, method, ownership, sourceResolution }) =>
|
||||
`promise:${kind}:${method}:${ownership}:${sourceResolution}`,
|
||||
|
|
|
|||
|
|
@ -51,3 +51,16 @@ it("preserves source-tree enter/exit order and detached ancestor snapshots", ()
|
|||
],
|
||||
]);
|
||||
});
|
||||
|
||||
it("skips sparse child slots without disturbing sibling visit order", () => {
|
||||
const parsed = parseJavaScriptSource("const values = [first, , second];");
|
||||
if (parsed === null)
|
||||
throw new TypeError("Expected a parsed JavaScript fixture");
|
||||
const identifiers: string[] = [];
|
||||
traverseJavaScriptAst(parsed.program, {
|
||||
enter: (node) => {
|
||||
if (t.isIdentifier(node)) identifiers.push(node.name);
|
||||
},
|
||||
});
|
||||
expect(identifiers).toEqual(["values", "first", "second"]);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -51,11 +51,19 @@ interface TraversalFrame {
|
|||
}
|
||||
|
||||
const childNodes = (node: t.Node): t.Node[] => {
|
||||
const keys: readonly string[] = t.VISITOR_KEYS[node.type] ?? [];
|
||||
return keys.flatMap((key) => {
|
||||
const keys = t.VISITOR_KEYS[node.type];
|
||||
if (keys === undefined) return [];
|
||||
const children: t.Node[] = [];
|
||||
for (const key of keys) {
|
||||
const value: unknown = Reflect.get(node, key);
|
||||
if (t.isNode(value)) return [value];
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value.filter((item): item is t.Node => t.isNode(item));
|
||||
});
|
||||
if (t.isNode(value)) {
|
||||
children.push(value);
|
||||
continue;
|
||||
}
|
||||
if (!Array.isArray(value)) continue;
|
||||
for (const item of value) {
|
||||
if (t.isNode(item)) children.push(item);
|
||||
}
|
||||
}
|
||||
return children;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,63 @@
|
|||
import { expect, it } from "vitest";
|
||||
|
||||
import { collectJavaScriptExports } from "./javascriptAstFingerprint.js";
|
||||
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
|
||||
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
||||
|
||||
const memberChain = ".next".repeat(12_000);
|
||||
|
||||
it("collects a 12,000-member CommonJS export path without recursion", () => {
|
||||
const file = parseJavaScriptSource(`exports${memberChain}.last = 1;`);
|
||||
if (file === null) throw new Error("Expected valid JavaScript");
|
||||
expect(collectJavaScriptExports(file).values).toEqual([
|
||||
`${"next.".repeat(12_000)}last`,
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves empty, dynamic, this, and private member path behavior", () => {
|
||||
const file = parseJavaScriptSource(`
|
||||
exports[""].value = 1;
|
||||
exports[""] = 0;
|
||||
exports[key].value = 2;
|
||||
this.exports.ignored = 3;
|
||||
class Example { #private = 0; read() { this.#private = 1; } }
|
||||
`);
|
||||
if (file === null) throw new Error("Expected valid JavaScript");
|
||||
expect(collectJavaScriptExports(file).values).toEqual([
|
||||
"",
|
||||
".value",
|
||||
"key.value",
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps left-first and outer-member-first bundler runtime matches", () => {
|
||||
const source = `
|
||||
(globalThis.webpackChunkLeft || globalThis.webpackChunkRight).push([[1], { 1: function() {} }]);
|
||||
globalThis.webpackChunkOuter.webpackChunkInner.push([[2], { 2: function() {} }]);
|
||||
`;
|
||||
const analysis = analyzeJavaScriptStaticSource(source);
|
||||
expect(
|
||||
analysis.bundler_registrations.find(({ chunk_keys }) =>
|
||||
chunk_keys.includes("1"),
|
||||
)?.runtime,
|
||||
).toBe("webpackChunkLeft");
|
||||
expect(
|
||||
analysis.bundler_registrations.find(({ chunk_keys }) =>
|
||||
chunk_keys.includes("2"),
|
||||
)?.runtime,
|
||||
).toBe("webpackChunkInner");
|
||||
});
|
||||
|
||||
it("analyzes a 12,000-member bundler runtime chain without recursion", () => {
|
||||
const source = `globalThis.webpackChunkApp${memberChain}.push([[1], { 1: function(module) { module.exports.ok = 42; } }]);`;
|
||||
const analysis = analyzeJavaScriptStaticSource(source);
|
||||
expect(analysis.parse_status).toBe("complete");
|
||||
expect(analysis.parse_error_count).toBe(0);
|
||||
expect(analysis.bundler_registrations).toHaveLength(1);
|
||||
expect(analysis.bundler_registrations[0]).toMatchObject({
|
||||
bundler: "webpack",
|
||||
runtime: "webpackChunkApp",
|
||||
chunk_keys: ["1"],
|
||||
modules: [expect.objectContaining({ exports: ["ok"] })],
|
||||
});
|
||||
});
|
||||
|
|
@ -73,19 +73,38 @@ export const chunkRuntime = (call: t.CallExpression): string | undefined => {
|
|||
};
|
||||
|
||||
const findChunkRuntime = (node: t.Node): string | undefined => {
|
||||
if (t.isIdentifier(node) && /(?:webpack|rspack)Chunk/iu.test(node.name))
|
||||
return node.name;
|
||||
if (t.isMemberExpression(node) || t.isOptionalMemberExpression(node)) {
|
||||
const property = semanticStaticPropertyName(node.property, node.computed);
|
||||
if (/(?:webpack|rspack)Chunk/iu.test(property)) return property;
|
||||
return t.isNode(node.object) ? findChunkRuntime(node.object) : undefined;
|
||||
const pending: t.Node[] = [node];
|
||||
while (pending.length > 0) {
|
||||
const current = pending.pop();
|
||||
if (current === undefined) continue;
|
||||
if (
|
||||
t.isIdentifier(current) &&
|
||||
/(?:webpack|rspack)Chunk/iu.test(current.name)
|
||||
)
|
||||
return current.name;
|
||||
if (
|
||||
t.isMemberExpression(current) ||
|
||||
t.isOptionalMemberExpression(current)
|
||||
) {
|
||||
const property = semanticStaticPropertyName(
|
||||
current.property,
|
||||
current.computed,
|
||||
);
|
||||
if (/(?:webpack|rspack)Chunk/iu.test(property)) return property;
|
||||
if (t.isNode(current.object)) pending.push(current.object);
|
||||
continue;
|
||||
}
|
||||
if (t.isAssignmentExpression(current)) {
|
||||
pending.push(current.right, current.left);
|
||||
continue;
|
||||
}
|
||||
if (t.isLogicalExpression(current) || t.isBinaryExpression(current)) {
|
||||
pending.push(current.right, current.left);
|
||||
continue;
|
||||
}
|
||||
if (t.isParenthesizedExpression(current) || t.isTSAsExpression(current))
|
||||
pending.push(current.expression);
|
||||
}
|
||||
if (t.isAssignmentExpression(node))
|
||||
return findChunkRuntime(node.left) ?? findChunkRuntime(node.right);
|
||||
if (t.isLogicalExpression(node) || t.isBinaryExpression(node))
|
||||
return findChunkRuntime(node.left) ?? findChunkRuntime(node.right);
|
||||
if (t.isParenthesizedExpression(node) || t.isTSAsExpression(node))
|
||||
return findChunkRuntime(node.expression);
|
||||
return undefined;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,13 +3,13 @@ import { createHash } from "node:crypto";
|
|||
import canonicalize from "canonicalize";
|
||||
import { z } from "zod";
|
||||
|
||||
import { evidenceSchema, parseEvidence, type Evidence } from "./evidence.js";
|
||||
import { evidenceSchema, parseEvidence, type Evidence } from "../evidence.js";
|
||||
import {
|
||||
createJavaScriptApplicationGraph,
|
||||
javascriptApplicationGraphSchema,
|
||||
type ApplicationEdge,
|
||||
type ApplicationNode,
|
||||
} from "./javascript/javascriptApplicationGraph.js";
|
||||
} from "../javascript/javascriptApplicationGraph.js";
|
||||
import {
|
||||
managedArtifactInspectionSchema,
|
||||
managedMemberInspectionSchema,
|
||||
|
|
@ -18,7 +18,7 @@ import {
|
|||
type ManagedMemberInspection,
|
||||
type ManagedNativeBoundaryInspection,
|
||||
} from "./managedArtifact.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import {
|
||||
assessManagedGraphOmissions,
|
||||
managedGraphEvidenceCoverage,
|
||||
|
|
@ -30,8 +30,8 @@ import {
|
|||
addBoundaryNodes,
|
||||
addMemberNodes,
|
||||
} from "./managedApplicationGraphNodes.js";
|
||||
import { digestSchema } from "./../domain/digests.js";
|
||||
import { prefixedDigestSchema } from "./../domain/digests.js";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { prefixedDigestSchema } from "../digests.js";
|
||||
|
||||
const evidenceIdSchema = prefixedDigestSchema("ev");
|
||||
const textSchema = z.string().min(1);
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ApplicationGraphEvidence } from "./javascript/javascriptApplicationGraph.js";
|
||||
import type { ApplicationGraphEvidence } from "../javascript/javascriptApplicationGraph.js";
|
||||
import type {
|
||||
ManagedArtifactInspection,
|
||||
ManagedMemberInspection,
|
||||
|
|
@ -4,7 +4,7 @@ import {
|
|||
createJavaScriptApplicationNode,
|
||||
type ApplicationGraphEvidence,
|
||||
type ApplicationNode,
|
||||
} from "./javascript/javascriptApplicationGraph.js";
|
||||
} from "../javascript/javascriptApplicationGraph.js";
|
||||
import { managedSourceCoverage } from "./managedApplicationGraphCoverage.js";
|
||||
import {
|
||||
type ManagedMemberInspection,
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { z } from "zod";
|
||||
import { digestSchema } from "./../domain/digests.js";
|
||||
import { digestSchema } from "../digests.js";
|
||||
|
||||
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
|
||||
const offsetSchema = z.number().int().min(0);
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { emptyArraySchema } from "./emptyArraySchema.js";
|
||||
import { evidenceSchema } from "./evidence.js";
|
||||
import { emptyArraySchema } from "../emptyArraySchema.js";
|
||||
import { evidenceSchema } from "../evidence.js";
|
||||
import {
|
||||
cliMetadataGuidSchema,
|
||||
type ManagedMemberInspection,
|
||||
|
|
@ -18,8 +18,8 @@ import {
|
|||
buildMethodItems,
|
||||
} from "./managedMemberComparisonItems.js";
|
||||
import { keyMembers, sha256 } from "./managedMemberComparisonMatch.js";
|
||||
import { digestSchema } from "./../domain/digests.js";
|
||||
import { prefixedDigestSchema } from "./../domain/digests.js";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { prefixedDigestSchema } from "../digests.js";
|
||||
|
||||
const evidenceIdSchema = prefixedDigestSchema("ev");
|
||||
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
|
||||
|
|
@ -2,7 +2,7 @@ import { createHash } from "node:crypto";
|
|||
|
||||
import canonicalize from "canonicalize";
|
||||
|
||||
import { parseEvidence } from "./evidence.js";
|
||||
import { parseEvidence } from "../evidence.js";
|
||||
import {
|
||||
managedMemberInspectionSchema,
|
||||
type ManagedMemberInspection,
|
||||
|
|
@ -11,7 +11,7 @@ import type {
|
|||
ManagedMemberComparisonResult,
|
||||
ManagedMemberComparisonSide,
|
||||
} from "./managedMemberComparison.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
|
||||
export const sha256 = (value: JsonValue): string => {
|
||||
const serialized = canonicalize(value);
|
||||
|
|
@ -2,12 +2,12 @@ import { createHash } from "node:crypto";
|
|||
|
||||
import canonicalize from "canonicalize";
|
||||
|
||||
import { parseEvidence } from "./evidence.js";
|
||||
import { parseEvidence } from "../evidence.js";
|
||||
import {
|
||||
managedNativeBoundaryInspectionSchema,
|
||||
type ManagedNativeBoundaryInspection,
|
||||
} from "./managedArtifact.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import {
|
||||
managedNativeVerificationResultSchema,
|
||||
type ManagedNativeVerificationInput,
|
||||
|
|
@ -3,11 +3,11 @@ import { basename } from "node:path";
|
|||
|
||||
import canonicalize from "canonicalize";
|
||||
|
||||
import { parseEvidence } from "./evidence.js";
|
||||
import { functionDossierSchema } from "./hopperValues.js";
|
||||
import { inspectMachoSchema } from "./nativeInspection.js";
|
||||
import { parseEvidence } from "../evidence.js";
|
||||
import { functionDossierSchema } from "../hopperValues.js";
|
||||
import { inspectMachoSchema } from "../nativeInspection.js";
|
||||
import type { ManagedNativeBoundaryInspection } from "./managedArtifact.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import {
|
||||
pinvokeVerificationSchema,
|
||||
type ManagedNativeVerificationInput,
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { z } from "zod";
|
||||
|
||||
import { emptyArraySchema } from "./emptyArraySchema.js";
|
||||
import { evidenceSchema } from "./evidence.js";
|
||||
import { emptyArraySchema } from "../emptyArraySchema.js";
|
||||
import { evidenceSchema } from "../evidence.js";
|
||||
import { cliMetadataGuidSchema } from "./managedArtifact.js";
|
||||
import { digestSchema } from "./../domain/digests.js";
|
||||
import { prefixedDigestSchema } from "./../domain/digests.js";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { prefixedDigestSchema } from "../digests.js";
|
||||
|
||||
const evidenceIdSchema = prefixedDigestSchema("ev");
|
||||
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createEvidence } from "./evidence.js";
|
||||
import { createEvidence } from "../evidence.js";
|
||||
import { managedReconstructionImportInputSchema } from "./managedReconstruction.js";
|
||||
|
||||
describe("managed reconstruction import input", () => {
|
||||
|
|
@ -3,15 +3,15 @@ import { createHash } from "node:crypto";
|
|||
import canonicalize from "canonicalize";
|
||||
import { z } from "zod";
|
||||
|
||||
import { evidenceSchema, parseEvidence } from "./evidence.js";
|
||||
import { evidenceSchema, parseEvidence } from "../evidence.js";
|
||||
import {
|
||||
cliMetadataGuidSchema,
|
||||
managedMemberInspectionSchema,
|
||||
type ManagedMemberInspection,
|
||||
} from "./managedArtifact.js";
|
||||
import type { JsonValue } from "./jsonValue.js";
|
||||
import { digestSchema } from "./../domain/digests.js";
|
||||
import { prefixedDigestSchema } from "./../domain/digests.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import { digestSchema } from "../digests.js";
|
||||
import { prefixedDigestSchema } from "../digests.js";
|
||||
|
||||
const evidenceIdSchema = prefixedDigestSchema("ev");
|
||||
const tokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u);
|
||||
|
|
@ -166,6 +166,7 @@ describe("Interface Builder dispatch joins", () => {
|
|||
const joined = joinInterfaceBuilderDispatch(ui.graph, {
|
||||
objc_classes: [],
|
||||
objc_protocols: [],
|
||||
objc_categories: [],
|
||||
swift_decls: [],
|
||||
objc_ivars: [],
|
||||
objc_protocol_records: [],
|
||||
|
|
@ -227,6 +228,7 @@ describe("Interface Builder dispatch joins", () => {
|
|||
const metadata = {
|
||||
objc_classes: [],
|
||||
objc_protocols: [],
|
||||
objc_categories: [],
|
||||
swift_decls: [],
|
||||
objc_ivars: [],
|
||||
objc_protocol_records: [],
|
||||
|
|
@ -304,6 +306,7 @@ describe("placeholder dispatch joins", () => {
|
|||
const joined = joinInterfaceBuilderDispatch(ui.graph, {
|
||||
objc_classes: [],
|
||||
objc_protocols: [],
|
||||
objc_categories: [],
|
||||
swift_decls: [],
|
||||
objc_ivars: [],
|
||||
objc_protocol_records: [],
|
||||
|
|
|
|||
|
|
@ -156,6 +156,8 @@ export const objcDispatchImplementationSchema = z.strictObject({
|
|||
selector: z.string().min(1),
|
||||
method_type: z.enum(["instance", "class"]),
|
||||
implementation_address: z.string().nullable(),
|
||||
/** Category that adds this method, when it is not defined by the class itself. */
|
||||
category: z.string().nullable().optional(),
|
||||
location: nativeMetadataLocationSchema,
|
||||
decode: nativeMetadataDecodeSchema,
|
||||
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
|
||||
|
|
@ -164,6 +166,21 @@ export type ObjcDispatchImplementation = z.infer<
|
|||
typeof objcDispatchImplementationSchema
|
||||
>;
|
||||
|
||||
/** Objective-C category: methods, protocols and properties added to a class. */
|
||||
export const objcCategorySchema = z.strictObject({
|
||||
name: z.string().min(1),
|
||||
/** Extended class; external classes come from the category's bind symbol. */
|
||||
class_name: z.string().min(1).nullable(),
|
||||
class_source: z.enum(["local", "external", "unresolved"]),
|
||||
instance_methods: z.array(z.string()),
|
||||
class_methods: z.array(z.string()),
|
||||
protocols: z.array(z.string()),
|
||||
properties: z.array(objcPropertySchema),
|
||||
location: nativeMetadataLocationSchema,
|
||||
decode: nativeMetadataDecodeSchema,
|
||||
evidence: z.array(nativeMetadataEvidenceSchema).min(1),
|
||||
});
|
||||
|
||||
/** Swift protocol conformance and its associated witness-table location. */
|
||||
export const swiftConformanceSchema = z.strictObject({
|
||||
type_name: z.string().min(1),
|
||||
|
|
@ -279,6 +296,7 @@ export type DbSaveResult = z.infer<typeof dbSaveResultSchema>;
|
|||
export const objcSwiftMetadataSchema = z.strictObject({
|
||||
objc_classes: z.array(objcClassSchema).default([]),
|
||||
objc_protocols: z.array(objcProtocolSchema).default([]),
|
||||
objc_categories: z.array(objcCategorySchema).default([]),
|
||||
swift_decls: z.array(swiftDeclSchema).default([]),
|
||||
objc_ivars: z.array(objcIvarSchema).default([]),
|
||||
objc_protocol_records: z.array(objcProtocolRecordSchema).default([]),
|
||||
|
|
|
|||
|
|
@ -28,13 +28,37 @@ describe("direct process scenarios", () => {
|
|||
});
|
||||
|
||||
it("rejects malformed environment names and adapter-reserved identity", () => {
|
||||
for (const name of ["", "KEY=VALUE", "KEY\0VALUE", "REA_PROCESS_RUN_ID"])
|
||||
expect(
|
||||
processScenarioSchema.safeParse({
|
||||
executable: "node",
|
||||
environment: { [name]: "value" },
|
||||
}).success,
|
||||
).toBe(false);
|
||||
for (const name of ["", "KEY=VALUE", "KEY\0VALUE", "REA_PROCESS_RUN_ID"]) {
|
||||
const result = processScenarioSchema.safeParse({
|
||||
executable: "node",
|
||||
environment: { [name]: "value" },
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
if (name === "" && !result.success) {
|
||||
const invalidKey = result.error.issues[0];
|
||||
expect(invalidKey?.code).toBe("invalid_key");
|
||||
if (invalidKey?.code === "invalid_key") {
|
||||
expect(invalidKey.issues[0]?.message).toBe(
|
||||
"Environment names must not be empty",
|
||||
);
|
||||
}
|
||||
}
|
||||
if (name === "REA_PROCESS_RUN_ID" && !result.success) {
|
||||
const invalidKey = result.error.issues[0];
|
||||
expect(invalidKey?.code).toBe("invalid_key");
|
||||
if (invalidKey?.code === "invalid_key") {
|
||||
expect(invalidKey.issues[0]?.message).toBe(
|
||||
"REA_PROCESS_RUN_ID is reserved by the process adapter",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(
|
||||
processScenarioSchema.safeParse({
|
||||
executable: "node",
|
||||
environment: { "REA_PROCESS_RUN_ID\n": "caller-value" },
|
||||
}).success,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps bounded capture controls and ordered scheduled interaction", () => {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,16 @@ import { z } from "zod";
|
|||
|
||||
const positiveBudget = z.number().int().safe().positive();
|
||||
const timedEventBase = { at_ms: z.number().int().safe().nonnegative() };
|
||||
const reservedRunIdEnvironmentName = "REA_PROCESS_RUN_ID";
|
||||
const environmentName = z
|
||||
.string()
|
||||
.regex(/^[^=\0]+$/u, "Environment names cannot contain '=' or NUL");
|
||||
const reservedEnvironment = new Set(["REA_PROCESS_RUN_ID"]);
|
||||
.min(1, "Environment names must not be empty")
|
||||
.regex(/^(?!REA_PROCESS_RUN_ID(?![\s\S]))[^=\0]+$/u, {
|
||||
error: (issue) =>
|
||||
issue.input === reservedRunIdEnvironmentName
|
||||
? `${reservedRunIdEnvironmentName} is reserved by the process adapter`
|
||||
: "Environment names cannot contain '=' or NUL",
|
||||
});
|
||||
export const normalizationSchema = z.object({
|
||||
paths: z.boolean(),
|
||||
pids: z.boolean(),
|
||||
|
|
@ -170,16 +176,6 @@ export const processScenarioSchema = z
|
|||
});
|
||||
}
|
||||
}
|
||||
const explicit = new Set(Object.keys(scenario.environment));
|
||||
for (const name of explicit) {
|
||||
if (reservedEnvironment.has(name)) {
|
||||
context.addIssue({
|
||||
code: "custom",
|
||||
message: `${name} is reserved by the process adapter`,
|
||||
path: ["environment", name],
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const event of scenario.events) {
|
||||
if (event.at_ms > scenario.timeout_ms) {
|
||||
context.addIssue({
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
managedArtifactInspectionSchema,
|
||||
type ManagedArtifactInspection,
|
||||
type ManagedParseIssue,
|
||||
} from "../domain/managedArtifact.js";
|
||||
} from "../domain/managed/managedArtifact.js";
|
||||
import {
|
||||
readManagedPeLayout,
|
||||
type ManagedCliHeader,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
managedMemberInspectionSchema,
|
||||
type ManagedMemberInspection,
|
||||
type ManagedParseIssue,
|
||||
} from "../domain/managedArtifact.js";
|
||||
} from "../domain/managed/managedArtifact.js";
|
||||
import {
|
||||
readManagedPeLayout,
|
||||
type ManagedPeLayout,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import {
|
|||
managedFailure,
|
||||
ManagedReaderFailure,
|
||||
} from "./ManagedReaderFailure.js";
|
||||
import type { ManagedMemberInspection } from "../domain/managedArtifact.js";
|
||||
import type { ManagedMemberInspection } from "../domain/managed/managedArtifact.js";
|
||||
import {
|
||||
type ManagedMetadataLayout,
|
||||
type MetadataTableLayout,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type {
|
||||
ManagedArtifactInspection,
|
||||
ManagedParseIssue,
|
||||
} from "../domain/managedArtifact.js";
|
||||
} from "../domain/managed/managedArtifact.js";
|
||||
import {
|
||||
METADATA_TABLE_NAMES,
|
||||
type ManagedMetadataLayout,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type {
|
||||
ManagedArtifactInspection,
|
||||
ManagedParseIssue,
|
||||
} from "../domain/managedArtifact.js";
|
||||
} from "../domain/managed/managedArtifact.js";
|
||||
import {
|
||||
metadataRowOffset,
|
||||
type ManagedMetadataLayout,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
managedNativeBoundaryInspectionSchema,
|
||||
type ManagedNativeBoundaryInspection,
|
||||
type ManagedParseIssue,
|
||||
} from "../domain/managedArtifact.js";
|
||||
} from "../domain/managed/managedArtifact.js";
|
||||
import { type ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
|
||||
import {
|
||||
metadataRowCursor,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import {
|
|||
managedNativeBoundaryInspectionSchema,
|
||||
type ManagedNativeBoundaryInspection,
|
||||
type ManagedParseIssue,
|
||||
} from "../domain/managedArtifact.js";
|
||||
} from "../domain/managed/managedArtifact.js";
|
||||
import {
|
||||
readManagedMetadataInventory,
|
||||
type ManagedResourceDirectory,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue