Merge origin/main into feat/artifact-integrity-policy

Keep both new MCP extraction tests, use main's relocated Apple domain
imports, and regenerate the catalog metadata.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822
2026-10-07 13:41:30 +00:00
co-authored by Claude Opus 5.5
228 changed files with 7188 additions and 1390 deletions
+8 -10
View File
@@ -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
@@ -302,12 +305,13 @@ jobs:
- run: npm ci
- run: npm run verify:keyed-archive
- run: npm run verify:asset-catalog
- run: npm run verify:macos-bundle
- name: Native compiled NIB hierarchy regressions
run: npm run test:focused -- tests/boundary/filesystem/nibNativeHierarchy.test.ts tests/boundary/filesystem/interfaceBuilderBundle.test.ts
- name: Real binary metadata and Interface Builder integration
run: npm run verify:apple-dispatch && npm run verify:interface-builder
- name: Native plist CLI selection regressions
run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts
run: npm run test:focused -- tests/boundary/cli/nativePlistCli.test.ts tests/boundary/cli/nativePlistPrototypeKeyCli.test.ts tests/boundary/cli/nativePlistSelectionFailureCli.test.ts
reconstruction-readiness:
name: Reconstruction readiness
@@ -353,12 +357,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
+3
View File
@@ -6,6 +6,7 @@ on:
paths:
- "website/**"
- "scripts/verify-website.py"
- "scripts/prepare-website.py"
- ".github/workflows/**"
workflow_dispatch:
@@ -24,5 +25,7 @@ jobs:
steps:
- name: Check out the repository
uses: actions/checkout@v4
- name: Prepare the example download
run: python3 scripts/prepare-website.py
- name: Check website links and publishing configuration
run: python3 scripts/verify-website.py
+2
View File
@@ -21,6 +21,8 @@ jobs:
steps:
- name: Check out the repository
uses: actions/checkout@v4
- name: Prepare the example download
run: python3 scripts/prepare-website.py
- name: Check website links and publishing configuration
run: python3 scripts/verify-website.py
- name: Configure Pages
+3
View File
@@ -45,3 +45,6 @@ Thumbs.db
# Local reference sources and analysis artifacts
/_reference/
# Generated website download
/website/public/examples/notes-example.zip
+4 -2
View File
@@ -17,7 +17,9 @@
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;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>
@@ -163,7 +165,7 @@ Deep native binary analysis requires [Hopper](https://www.hopperapp.com/), [Ghid
Firmware region inspection and explicit extraction use caller-supplied Binwalk and Unblob on Linux. See [Firmware analysis](docs/firmware-analysis.md) for setup, provenance, resource limits and native handoff.
Static APK analysis uses a separately supplied headless JADX JAR and a full JDK, with no emulator or APK execution. See [Android analysis](docs/android-analysis.md) for setup, CLI/MCP operations, coverage and public test fixtures. Authenticated IPA inventory Evidence can be projected with [Apple application analysis](docs/apple-application-analysis.md).
Static APK analysis uses a separately supplied headless JADX JAR and a full JDK, with no emulator or APK execution. See [Android analysis](docs/android-analysis.md) for setup, CLI/MCP operations, coverage and public test fixtures. Authenticated IPA and macOS `.app`, ZIP, or DMG inventory Evidence can be projected into bundle anatomy, such as XPC services, app extensions, login items, privileged helpers, and launchd plists, with [Apple application analysis](docs/apple-application-analysis.md).
Repository main and npm 4.1.0 include experimental Windows x64 Ghidra support for native x86-64 PE applications on local NTFS, with bundled Job Object, private-DACL, and path-admission controls. Check the [release boundary](docs/installation.md#released-package-and-main) before expecting this from an older npm package. See [Windows Ghidra P0](docs/windows-ghidra-p0.md) for prerequisites and verified scope.
+2
View File
@@ -17,6 +17,8 @@
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;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">
+2
View File
@@ -17,6 +17,8 @@
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;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">
+2
View File
@@ -17,6 +17,8 @@
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;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">
+2
View File
@@ -17,6 +17,8 @@
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&amp;utm_medium=badge&amp;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">
+11
View File
@@ -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)
}
+11 -7
View File
@@ -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()
+85 -4
View File
@@ -1,10 +1,11 @@
# Apple Application Inventory Projection
REA can project one or more authenticated `inventory_artifact` Evidence records
for the same IPA into an Apple application inventory. The projection is
execution-free: it reports exact paths, content hashes, detected formats,
runtime-family hints, and path-based bridge hypotheses. It does not parse plist
or CMS semantics or claim observed runtime calls.
for the same IPA, macOS `.app` directory, ZIP, or DMG into an Apple application
inventory. The projection is execution-free: it reports exact paths, content
hashes, detected formats, bundle anatomy, runtime-family hints, and path-based
bridge hypotheses. It does not parse plist or CMS semantics or claim observed
runtime calls.
```sh
rea project-apple-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
@@ -29,3 +30,83 @@ Coverage is `complete-within-inventory` when the supplied pages reconstruct the
complete authenticated inventory, and `partial` otherwise. A partial result
states that absence is unknown. Component and bridge-candidate arrays are still
the complete projection of the inventory that was supplied.
## Application roots
| Inventory root | Application root |
| -------------------------------------- | --------------------------------------------------- |
| IPA | Each `Payload/<Name>.app` |
| `.app` directory | `.`, the inventoried bundle itself |
| Directory, ZIP, or DMG containing apps | Each outermost `<Name>.app` followed by `Contents/` |
ZIP archives made with `ditto -c -k --keepParent` report `<Name>.app`. A DMG
reports `<image>.dmg/<Volume>/<Name>.app`; DMG child inventory needs the native
macOS mounting adapter. When a DMG has only its root identity, the projection is
`partial` and says that bundle absence is unknown. Other inventory formats, such
as APK or ASAR, are rejected as input errors.
An inventoried directory whose name ends in `.app` is its own root even when its
inventory is partial or lacks `Contents/`. For directory, ZIP, and DMG
inventories, components outside every application root are not attributed to
the application. This covers installers, disk-image extras, and AppleDouble
sidecars, which therefore stay out of runtime families and bridge candidates. A
limitation reports how many inventoried entries were left out. IPA projection
still lists every archive component, including `SwiftSupport/`.
`platforms` lists `macos` for application roots with `Contents/`. It lists `ios`
for shallow application roots that have an `Info.plist`. An application root
with neither, such as an empty inventory, has no platform.
## macOS bundle anatomy
`bundles` lists each application root and every nested bundle under it. Each
bundle reports its `parent_path`, `layout`, `info_plist_path`,
`executable_candidates`, and `signing_paths`.
| Layout | Meaning | Content directory |
| --------------------- | --------------- | --------------------------------------------- |
| `macos-deep` | Has `Contents/` | `Contents/`, executables in `Contents/MacOS/` |
| `versioned-framework` | Has `Versions/` | Each real `Versions/<version>/` |
| `shallow` | Neither | The bundle directory itself |
Roles are path conventions (`role_basis: "path-convention"`):
| Role | Path |
| -------------------- | --------------------------------------------------------------- |
| `app-extension` | `*.appex` |
| `xpc-service` | `*.xpc`, including XPC services inside frameworks |
| `framework` | `*.framework` |
| `system-extension` | `*.systemextension` |
| `driver-extension` | `*.dext` |
| `plug-in` | `PlugIns/*.bundle`, `*.plugin`, `*.qlgenerator`, `*.mdimporter` |
| `resource-bundle` | `Resources/*.bundle`, such as SwiftPM resource bundles |
| `bundle` | Any other `*.bundle` |
| `login-item` | `Contents/Library/LoginItems/*.app` |
| `helper-application` | `Contents/Helpers/*.app` |
| `nested-application` | Any other nested `*.app` |
The components also list:
- `privileged_helpers`: files directly in `Contents/Library/LaunchServices/`,
where SMJobBless helpers live;
- `launchd_plists`: `.plist` files directly in `Contents/Library/LaunchAgents/`
(`domain: "agent"`) or `Contents/Library/LaunchDaemons/` (`domain: "daemon"`),
as used by SMAppService;
- `helpers`: files directly in `Contents/Helpers/`.
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`. 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
anatomy components.
The projection does not read plists. Use `inspect_plist` on a bundle's
`info_plist_path` or a launchd plist to read `CFBundleExecutable`, identifiers,
`SMPrivilegedExecutables`, `BundleProgram`, URL schemes, and usage descriptions.
Use `inspect_signature` on an executable candidate for code-signing claims.
+53
View File
@@ -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
@@ -332,3 +356,32 @@ repository-root default. Both point to `_reference/firmware-integration/generate
when invoked through the documented npm commands. Optional ext4/Ghidra lanes keep
their own prerequisites. Source-fixture generation and synthetic producer tests
do not establish real Binwalk/Unblob analysis or another host's execution support.
## Apple artifact capability ownership
`src/domain/apple/` owns Apple application anatomy/projection, asset catalog
facts, Interface Builder graphs, keyed archive semantics and plist values.
`src/application/apple/AppleApplicationService.ts` retains the shared workflow
that projects authenticated IPA/macOS inventory Evidence.
`src/artifacts/apple/` owns the actual producer boundary: directory acquisition,
assetutil invocation, plist/NIB decoding, encoded view-parent interpretation and
keyed archive inspection. These helpers are used by ArtifactProvider, retain
its existing typed analysis boundary, injected assetutil seam and original reader
cleanup, and do not require a new port for each helper. Producer code no longer
lives in the application layer. Guards reject application/composition/caller
imports from this owner while admitting pure facts and shared artifact readers.
Adapter-source tests retain their assertions in the existing forked adapter lane.
`scripts/verify/apple/` groups archive, asset catalog, Interface Builder,
Objective-C/Swift dispatch and macOS bundle verification. The macOS bundle fixture
builder lives in `scripts/fixtures/apple/`; NIB byte fixtures stay beside their
artifact decoder. Existing format-specific conformance sources and goldens keep
their locations, and relocated verifiers resolve them against their own file
URLs. Public npm entrypoints and CLI/MCP contracts stay stable; generated-document
cache inputs include the relocated runtime scripts and fixture builder.
Portable decoder and injected producer tests establish their format/boundary
claims. Actual Swift/Xcode, assetutil, native NIB, bundle signing and DMG lifecycle
claims require the real Apple lane. Native binary API/value-flow semantics and
shared cross-domain contract aggregates keep their separate owners.
+18 -5
View File
@@ -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.
+2 -2
View File
@@ -669,9 +669,9 @@
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "967c3d60f973aeb86a90a5772a217bef90cc5e48d2eea9621d3a2cc2a68834eb",
"tools_sha256": "68769748b624a288a5f4c57374de2d6fa5c2b5f5586fbbdef109795c907b7b7b",
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
"combined_sha256": "e893b9df15ed6eec249c8f9adb583dc4870c240f9126f8e42680a014cd6fba90",
"combined_sha256": "44b30e8892c84cf27766d5739228268b8a85f885c63002157bc134ed8d636bb8",
"providers_sha256": "a7d885531ee25d42c51385444faec544d34cd7eba6250a7dc75c4faedd4454a3"
}
}
+49 -14
View File
@@ -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,
@@ -71,6 +81,19 @@ an XML graph golden. `verify:asset-catalog` compiles source-owned colors with
`actool`, invokes real `assetutil`, then checks CLI/MCP results, exact catalog
digest, every raw metadata field, pagination and malformed input rejection.
Neither artifact workflow requires Hopper or launches it. Both run in macOS CI.
`verify:macos-bundle` needs only macOS with Command Line Tools. It compiles a
source-owned app with `clang`: a versioned framework, XPC services, an app
extension, a login item, a privileged helper, launchd plists, and a helper tool,
signed ad hoc. It packs the app as a directory, a `ditto` ZIP, and an APFS DMG,
then checks that `inspect-artifact` plus `project-apple-application-graph`
report the same bundle anatomy for all three through the CLI, with stdio MCP
parity. It also checks that the DMG is detached afterwards. It runs in macOS CI.
Apple artifact verifiers live in `scripts/verify/apple/`, with the macOS bundle
builder under `scripts/fixtures/apple/` and NIB byte fixtures beside the decoder
in `src/artifacts/apple/`. The npm entrypoints are unchanged. Format-specific
Swift/XIB/asset sources and goldens retain their locations; real Apple workflows
resolve them from the verifier file URL and run in the macOS CI lane.
MCP SDK transport tests with recording providers remain integration tests.
They are useful for schema drift and failure projection but do not prove that
@@ -204,7 +227,7 @@ kept under ignored `_reference/`. No Gradle build, Android SDK, emulator or
application execution is required. The lane compares real CLI/MCP package,
class search, class inventory, method decompilation and incoming references.
See [Android analysis](android-analysis.md) for boundaries and resource budgets.
Authenticated IPA inventory projection is documented in
Authenticated IPA and macOS application inventory projection is documented in
[Apple application analysis](apple-application-analysis.md).
The lane and owned-process cancellation helper live in `scripts/verify/android/`.
@@ -320,8 +343,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
@@ -331,6 +354,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.
@@ -441,17 +469,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_1cd8dcd8881d15dcd8ff0829cb45f1cb14501e5de6c36f6c968a986b7c022daf",
"manifest_id": "ecm_b3da2d10df0f03a654528fe4101f68084d4b9476bba072508c30564c5468e349",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "d337de130efddaaee02ecef46a3e4ecb5ee2520ad9594a48ae4e471253c5f781"
"sha256": "10c7b7d80d72e8ec4faa3f17a57e4563907ecac5533d22645c43ba932f9dcc13"
}
],
"claims": [
+1
View File
@@ -5,6 +5,7 @@
"vitepress": {
"entry": ["docs/.vitepress/config.ts"]
},
"ignoreBinaries": ["mkfifo"],
"ignoreDependencies": ["electron", "esbuild", "webpack"],
"ignoreExportsUsedInFile": true,
"rules": {
+7 -5
View File
@@ -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",
@@ -120,16 +121,17 @@
"typecheck": "tsc --noEmit",
"verify:test-temp-hygiene": "npm run build:cached && node scripts/verify-test-temp-hygiene.mjs",
"verify:hopper": "npm run verify:fixtures && npm run build:cached && node scripts/verify-real-hopper.mjs",
"verify:apple-dispatch": "npm run build:cached && node scripts/verify-apple-dispatch.mjs",
"verify:apple-dispatch": "npm run build:cached && node scripts/verify/apple/dispatch.mjs",
"verify:native-ui": "npm run build:cached && node scripts/verify-native-ui.mjs",
"verify:keyed-archive": "npm run build:cached && node scripts/verify-keyed-archive.mjs",
"verify:keyed-archive": "npm run build:cached && node scripts/verify/apple/keyed-archive.mjs",
"verify:macos-bundle": "npm run build:cached && node scripts/verify/apple/macos-bundle.mjs",
"verify:ghidra": "npm run build:cached && node scripts/verify-real-ghidra.mjs",
"verify:ida": "npm run build:cached && node scripts/verify-real-ida.mjs",
"verify:ghidra:switch": "npm run build:cached && node scripts/verify-real-ghidra-switch.mjs",
"verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs",
"verify:ghidra:com": "npm run build:cached && node scripts/verify-real-ghidra-com.mjs",
"verify:ghidra:aarch64-jump-table": "npm run build:cached && node scripts/verify-real-ghidra.mjs --aarch64-jump-table",
"verify:interface-builder": "npm run build:cached && node scripts/verify-interface-builder.mjs",
"verify:interface-builder": "npm run build:cached && node scripts/verify/apple/interface-builder.mjs",
"verify:ghidra:cross-format": "npm run build:cached && node scripts/verify-real-ghidra.mjs --cross-format",
"verify:ghidra:windows": "npm run build:cached && npm run build:ghidra-windows-fixture && node scripts/verify-real-ghidra-windows.mjs",
"verify:browser": "npm run build:cached && node scripts/verify-real-browser.mjs",
@@ -140,7 +142,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",
@@ -169,7 +171,7 @@
"docs:error-schema": "npm run build:cached && node scripts/generate-error-schema.mjs",
"prepack": "node scripts/prepack.mjs",
"prepare": "node scripts/prepare.mjs",
"verify:asset-catalog": "npm run build:cached && node scripts/verify-asset-catalog.mjs",
"verify:asset-catalog": "npm run build:cached && node scripts/verify/apple/asset-catalog.mjs",
"verify:native-ui:permissions": "npm run build:cached && node scripts/verify-native-ui.mjs --allow-permission-denial",
"test:focused": "node scripts/run-development-tests.mjs focused",
"build:windows-native": "node scripts/build-windows-native.mjs",
+269
View File
@@ -0,0 +1,269 @@
import { execFile } from "node:child_process";
import { mkdir, rm, symlink, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
const exec = promisify(execFile);
/** Commands the macOS bundle fixture needs from Command Line Tools. */
export const MACOS_BUNDLE_FIXTURE_COMMANDS = [
["/usr/bin/xcrun", ["--find", "clang"]],
["/usr/bin/xcrun", ["--find", "codesign"]],
["/usr/bin/ditto", ["-h"]],
["/usr/bin/hdiutil", ["help"]],
];
/** Fail with the missing command name before building any fixture. */
export async function preflightMacosBundleFixture() {
if (process.platform !== "darwin")
throw new Error(
"macOS bundle verification requires a macOS host with Command Line Tools",
);
for (const [command, arguments_] of MACOS_BUNDLE_FIXTURE_COMMANDS)
try {
await exec(command, arguments_);
} catch (cause) {
// `ditto -h` and `hdiutil help` exit non-zero while printing usage.
if (cause?.code === "ENOENT")
throw new Error(
`macOS bundle verification requires ${command} ${arguments_.join(" ")}`,
{ cause },
);
if (command === "/usr/bin/xcrun")
throw new Error(
`macOS bundle verification requires Command Line Tools (${arguments_.join(" ")} failed)`,
{ cause },
);
}
}
const plist = (entries) =>
`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
${Object.entries(entries)
.map(([key, value]) => ` <key>${key}</key>\n <string>${value}</string>`)
.join("\n")}
</dict>
</plist>
`;
const bundlePlist = (identifier, executable, packageType = "APPL") =>
plist({
CFBundleIdentifier: identifier,
CFBundleExecutable: executable,
CFBundlePackageType: packageType,
});
const write = async (path, text) => {
await mkdir(dirname(path), { recursive: true });
await writeFile(path, text);
};
/** Compile one C source beneath `root/sources` into `output`. */
const clang = async (root, { name, source, output, flags = [] }) => {
const sourcePath = join(root, "sources", `${name}.c`);
await write(sourcePath, source);
await mkdir(dirname(output), { recursive: true });
await exec("/usr/bin/xcrun", [
"clang",
"-O0",
sourcePath,
"-o",
output,
...flags,
]);
};
const PROGRAM = `int main(void) { return 0; }\n`;
/** A versioned framework whose own rpath supplies a second `libchain.dylib`. */
const buildCoreFramework = async (root, frameworks) => {
const core = join(frameworks, "Core.framework");
const version = join(core, "Versions", "A");
const chainFlags = ["-dynamiclib", "-install_name", "@rpath/libchain.dylib"];
await clang(root, {
name: "chain-app",
source: `const char *chain_variant(void) { return "app"; }\n`,
output: join(frameworks, "libchain.dylib"),
flags: chainFlags,
});
await clang(root, {
name: "chain-framework",
source: `const char *chain_variant(void) { return "framework"; }\n`,
output: join(version, "Libraries", "libchain.dylib"),
flags: chainFlags,
});
await clang(root, {
name: "sibling",
source: `int sibling_value(void) { return 7; }\n`,
output: join(version, "libsibling.dylib"),
flags: ["-dynamiclib", "-install_name", "@loader_path/libsibling.dylib"],
});
await clang(root, {
name: "core",
source: `const char *chain_variant(void);
int sibling_value(void);
const char *core_chain(void) { return sibling_value() == 7 ? chain_variant() : "broken"; }
`,
output: join(version, "Core"),
flags: [
"-dynamiclib",
"-install_name",
"@rpath/Core.framework/Versions/A/Core",
"-Wl,-rpath,@loader_path/Libraries",
join(version, "libsibling.dylib"),
join(version, "Libraries", "libchain.dylib"),
],
});
await write(
join(version, "Resources", "Info.plist"),
bundlePlist("com.example.rea.core", "Core", "FMWK"),
);
const service = join(version, "XPCServices", "FwSvc.xpc", "Contents");
await clang(root, {
name: "framework-service",
source: PROGRAM,
output: join(service, "MacOS", "FwSvc"),
});
await write(
join(service, "Info.plist"),
bundlePlist("com.example.rea.core.service", "FwSvc", "XPC!"),
);
await symlink("A", join(core, "Versions", "Current"));
await symlink("Versions/Current/Core", join(core, "Core"));
await symlink("Versions/Current/Resources", join(core, "Resources"));
await symlink("Versions/Current/XPCServices", join(core, "XPCServices"));
return join(version, "Core");
};
/** The main executable, with a weak dependency deleted after linking. */
const buildMainExecutable = async (root, contents, coreBinary) => {
const gone = join(contents, "Frameworks", "libgone.dylib");
await clang(root, {
name: "gone",
source: `int gone_value(void) { return 1; }\n`,
output: gone,
flags: ["-dynamiclib", "-install_name", "@rpath/libgone.dylib"],
});
const executable = join(contents, "MacOS", "MacFixture");
await clang(root, {
name: "main",
source: `#include <stdio.h>
const char *core_chain(void);
extern int gone_value(void) __attribute__((weak_import));
int main(void) {
printf("chain=%s weak=%s\\n", core_chain(), gone_value ? "present" : "absent");
return 0;
}
`,
output: executable,
flags: [
"-Wl,-rpath,@executable_path/../Frameworks",
coreBinary,
`-Wl,-weak_library,${gone}`,
],
});
await rm(gone);
await write(
join(contents, "Info.plist"),
bundlePlist("com.example.rea.fixture", "MacFixture"),
);
return executable;
};
/** One `Contents/`-layout bundle with a trivial executable. */
const buildNestedBundle = async (root, { bundle, executable, plist }) => {
await clang(root, {
name: executable,
source: PROGRAM,
output: join(bundle, "Contents", "MacOS", executable),
});
await write(join(bundle, "Contents", "Info.plist"), plist);
};
const buildNestedBundles = async (root, contents, coreBinary) => {
const service = join(contents, "XPCServices", "Svc.xpc", "Contents");
await clang(root, {
name: "service",
source: `const char *core_chain(void);
int main(void) { return core_chain()[0] == 'b'; }
`,
output: join(service, "MacOS", "Svc"),
flags: ["-Wl,-rpath,@executable_path/../../../../Frameworks", coreBinary],
});
await write(
join(service, "Info.plist"),
bundlePlist("com.example.rea.fixture.service", "Svc", "XPC!"),
);
await buildNestedBundle(root, {
bundle: join(contents, "PlugIns", "Ext.appex"),
executable: "Ext",
plist: bundlePlist("com.example.rea.fixture.extension", "Ext", "XPC!"),
});
await buildNestedBundle(root, {
bundle: join(contents, "Library", "LoginItems", "Login.app"),
executable: "Login",
plist: bundlePlist("com.example.rea.fixture.login", "Login"),
});
};
/** A privileged helper, its launchd plists, and a non-bundle helper tool. */
const buildLaunchItems = async (root, contents) => {
await clang(root, {
name: "privileged-helper",
source: PROGRAM,
output: join(
contents,
"Library",
"LaunchServices",
"com.example.rea.helper",
),
});
await write(
join(contents, "Library", "LaunchDaemons", "com.example.rea.helper.plist"),
plist({
Label: "com.example.rea.helper",
BundleProgram: "Contents/Library/LaunchServices/com.example.rea.helper",
}),
);
await write(
join(contents, "Library", "LaunchAgents", "com.example.rea.agent.plist"),
plist({
Label: "com.example.rea.agent",
BundleProgram: "Contents/Helpers/rea-tool",
}),
);
await clang(root, {
name: "tool",
source: PROGRAM,
output: join(contents, "Helpers", "rea-tool"),
});
};
/**
* Build `MacFixture.app` with every bundle role and dylib-loading shape REA
* projects. The main executable prints which `libchain.dylib` copy dyld
* selected, so callers can compare static resolution with a real load.
*/
export async function buildMacosBundleFixture(root) {
const app = join(root, "MacFixture.app");
const contents = join(app, "Contents");
const coreBinary = await buildCoreFramework(
root,
join(contents, "Frameworks"),
);
const executable = await buildMainExecutable(root, contents, coreBinary);
await buildNestedBundles(root, contents, coreBinary);
await buildLaunchItems(root, contents);
await exec("/usr/bin/xcrun", [
"codesign",
"--force",
"--deep",
"--sign",
"-",
app,
]);
return { app, executable };
}
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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";
+2 -1
View File
@@ -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)
+7 -1
View File
@@ -15,6 +15,12 @@ const environment = () => ({
/** Execute the compiled CLI with production providers and no test adapters. */
export async function artifactCli(command, target, arguments_ = []) {
return (await artifactCliEvidence(command, target, arguments_))
.normalized_result;
}
/** Execute the compiled CLI and return its complete Evidence record. */
export async function artifactCliEvidence(command, target, arguments_ = []) {
const { stdout } = await promisify(execFile)(
process.execPath,
[
@@ -29,7 +35,7 @@ export async function artifactCli(command, target, arguments_ = []) {
const evidence = JSON.parse(stdout);
assert.equal(evidence.error, undefined, stdout);
assert.ok(evidence.normalized_result, "CLI omitted normalized result");
return evidence.normalized_result;
return evidence;
}
/** Run a real stdio MCP subprocess, owning and closing its transport. */
+6 -5
View File
@@ -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:
+1
View File
@@ -69,6 +69,7 @@ const failedBoundary = (file, target) => {
return "provider-construction";
if (
(file.startsWith("src/artifacts/javascript/") ||
file.startsWith("src/artifacts/apple/") ||
/^src\/artifacts\/ArtifactHash\.(?:js|ts)$/u.test(file)) &&
(["application", "composition", "server", "cli", "main"].includes(
targetLayer,
+40
View File
@@ -0,0 +1,40 @@
"""Package the public Notes example for the static website."""
from io import BytesIO
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
EXAMPLE_FILES = (
"package.json",
"main.js",
"preload.js",
"renderer.js",
"csv.js",
"index.html",
)
def prepare_notes_example(site):
"""Create a ZIP with public source only and reproducible metadata."""
source = site / "examples/notes-electron"
contents = BytesIO()
with ZipFile(contents, "w") as archive:
for name in EXAMPLE_FILES:
entry = ZipInfo(f"notes-example/{name}", (1980, 1, 1, 0, 0, 0))
entry.create_system = 3
entry.external_attr = 0o100644 << 16
archive.writestr(
entry,
(source / name).read_bytes(),
compress_type=ZIP_DEFLATED,
compresslevel=9,
)
destination = site / "examples/notes-example.zip"
destination.write_bytes(contents.getvalue())
print(f"Prepared examples/notes-example.zip ({len(EXAMPLE_FILES)} files).")
if __name__ == "__main__":
root = Path(__file__).resolve().parent.parent
prepare_notes_example(root / "website/public")
-133
View File
@@ -1,133 +0,0 @@
import { execFile } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { inspectAppleDispatchMetadata } from "../dist/native/AppleDispatchMetadata.js";
if (process.platform !== "darwin")
throw new Error(
"Apple dispatch verification requires macOS with a host Objective-C compiler and Foundation SDK",
);
const exec = promisify(execFile);
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,
});
}
process.stdout.write(
`${JSON.stringify({ ok: true, target_executed: false, fixtures: results })}\n`,
);
} finally {
await rm(root, { recursive: true, force: true });
}
+17 -3
View File
@@ -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 },
+1 -1
View File
@@ -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) => {
+26 -1
View File
@@ -6,6 +6,7 @@ import re
import sys
from urllib.parse import unquote, urlsplit
import xml.etree.ElementTree as ET
from zipfile import BadZipFile, ZipFile
class HtmlReferences(HTMLParser):
@@ -89,15 +90,39 @@ def check_publisher(root):
return []
def check_example_archive(site):
"""Verify the downloadable ZIP contains the current six source files."""
source = site / "examples/notes-electron"
expected = {
f"notes-example/{name}": source / name
for name in ("package.json", "main.js", "preload.js", "renderer.js", "csv.js", "index.html")
}
path = site / "examples/notes-example.zip"
if not path.is_file():
return ["Notes example ZIP is missing; run python3 scripts/prepare-website.py."]
try:
with ZipFile(path) as archive:
if sorted(archive.namelist()) != sorted(expected):
return ["Notes example ZIP must contain exactly the six files in notes-example/."]
return [
f"Notes example ZIP has stale source: {name}"
for name, original in expected.items()
if archive.read(name) != original.read_bytes()
]
except (BadZipFile, RuntimeError) as error:
return [f"Notes example ZIP cannot be read: {error}"]
def main():
"""Run the same checks locally, on website PRs and before publishing."""
root = Path(__file__).resolve().parent.parent
pages, references, errors = check_site(root / "website/public")
errors.extend(check_example_archive(root / "website/public"))
errors.extend(check_publisher(root))
if errors:
print("Website checks failed:\n" + "\n".join(errors), file=sys.stderr)
return 1
print(f"Verified {pages} HTML pages, {references} local references and SVG XML.")
print(f"Verified {pages} HTML pages, {references} local references, SVG XML and the example ZIP.")
print("website-pages.yml is the sole GitHub Pages publisher.")
return 0
@@ -10,7 +10,7 @@ import {
artifactCli,
artifactMcpResult,
withArtifactMcp,
} from "./lib/artifact-e2e.mjs";
} from "../../lib/artifact-e2e.mjs";
if (process.platform !== "darwin")
throw new Error(
@@ -30,7 +30,7 @@ try {
"clang",
fileURLToPath(
new URL(
"../tests/conformance/interface-builder/fixture.c",
"../../../tests/conformance/interface-builder/fixture.c",
import.meta.url,
),
),
@@ -44,7 +44,10 @@ try {
await exec("/usr/bin/xcrun", [
"actool",
fileURLToPath(
new URL("../tests/conformance/native/Assets.xcassets", import.meta.url),
new URL(
"../../../tests/conformance/native/Assets.xcassets",
import.meta.url,
),
),
"--compile",
resources,
+205
View File
@@ -0,0 +1,205 @@
import { execFile } from "node:child_process";
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js";
if (process.platform !== "darwin")
throw new Error(
"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 results = [];
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`,
);
} finally {
await rm(root, { recursive: true, force: true });
}
@@ -8,8 +8,8 @@ import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { ArtifactProvider } from "../dist/artifacts/ArtifactProvider.js";
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
import { ArtifactProvider } from "../../../dist/artifacts/ArtifactProvider.js";
const exec = promisify(execFile);
const fixtureRoot = await mkdtemp(join(tmpdir(), "rea-interface-builder-"));
@@ -18,7 +18,7 @@ const resourcesPath = join(appPath, "Contents", "Resources");
const executablePath = join(appPath, "Contents", "MacOS", "Fixture");
const nibPath = join(resourcesPath, "Main.nib");
const sourceRoot = fileURLToPath(
new URL("../tests/conformance/interface-builder/", import.meta.url),
new URL("../../../tests/conformance/interface-builder/", import.meta.url),
);
try {
@@ -10,7 +10,7 @@ import {
artifactCli,
artifactMcpResult,
withArtifactMcp,
} from "./lib/artifact-e2e.mjs";
} from "../../lib/artifact-e2e.mjs";
if (process.platform !== "darwin")
throw new Error(
@@ -25,7 +25,7 @@ try {
join(root, "modules"),
fileURLToPath(
new URL(
"../tests/conformance/native/keyed-archive.swift",
"../../../tests/conformance/native/keyed-archive.swift",
import.meta.url,
),
),
@@ -66,7 +66,7 @@ try {
const expected = JSON.parse(
await readFile(
new URL(
"../tests/fixtures/golden/keyed-archive/graph.json",
"../../../tests/fixtures/golden/keyed-archive/graph.json",
import.meta.url,
),
"utf8",
@@ -124,7 +124,7 @@ try {
join(root, "modules"),
fileURLToPath(
new URL(
"../tests/conformance/native/keyed-archive-uid-root.swift",
"../../../tests/conformance/native/keyed-archive-uid-root.swift",
import.meta.url,
),
),
+167
View File
@@ -0,0 +1,167 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import {
artifactCliEvidence,
artifactMcpResult,
withArtifactMcp,
} from "../../lib/artifact-e2e.mjs";
import {
buildMacosBundleFixture,
preflightMacosBundleFixture,
} from "../../fixtures/apple/macos-bundle.mjs";
const exec = promisify(execFile);
await preflightMacosBundleFixture();
const root = await mkdtemp(join(tmpdir(), "rea-macos-bundle-"));
/** Bundle roles relative to the application root, independent of the container. */
const anatomy = (projection, applicationRoot) => {
const relative = (path) =>
applicationRoot === "."
? path
: path === applicationRoot
? "."
: path.slice(applicationRoot.length + 1);
const paths = (components) =>
components.map(({ path }) => relative(path)).sort();
return {
bundles: projection.bundles.map((bundle) => ({
path: relative(bundle.path),
role: bundle.role,
layout: bundle.layout,
parent: bundle.parent_path === null ? null : relative(bundle.parent_path),
info_plist: bundle.info_plist_path && relative(bundle.info_plist_path),
executables: bundle.executable_candidates.map(relative),
signing: bundle.signing_paths.map(relative),
})),
privileged_helpers: paths(projection.components.privileged_helpers),
launchd_plists: projection.components.launchd_plists
.map(({ path, domain }) => `${domain}:${relative(path)}`)
.sort(),
helpers: paths(projection.components.helpers),
};
};
const FRAMEWORK = "Contents/Frameworks/Core.framework";
const EXPECTED_BUNDLES = [
[".", "application", "macos-deep", null],
[FRAMEWORK, "framework", "versioned-framework", "."],
[
`${FRAMEWORK}/Versions/A/XPCServices/FwSvc.xpc`,
"xpc-service",
"macos-deep",
FRAMEWORK,
],
["Contents/Library/LoginItems/Login.app", "login-item", "macos-deep", "."],
["Contents/PlugIns/Ext.appex", "app-extension", "macos-deep", "."],
["Contents/XPCServices/Svc.xpc", "xpc-service", "macos-deep", "."],
];
try {
const { app } = await buildMacosBundleFixture(root);
const zip = join(root, "MacFixture.zip");
await exec("/usr/bin/ditto", ["-c", "-k", "--keepParent", app, zip]);
const staging = join(root, "dmg-source");
await mkdir(staging);
await exec("/usr/bin/ditto", [app, join(staging, "MacFixture.app")]);
await symlink("/Applications", join(staging, "Applications"));
const dmg = join(root, "MacFixture.dmg");
// The default APFS image attaches as an image disk plus a synthesized
// container, which exercises detach of devices sharing one image.
await exec("/usr/bin/hdiutil", [
"create",
"-quiet",
"-srcfolder",
staging,
"-volname",
"ReaFixture",
"-format",
"UDZO",
dmg,
]);
const containers = [
{ target: app, format: "directory", applicationRoot: "." },
{ target: zip, format: "zip", applicationRoot: "MacFixture.app" },
{
target: dmg,
format: "dmg",
applicationRoot: "MacFixture.dmg/ReaFixture/MacFixture.app",
},
];
const anatomies = [];
for (const { target, format, applicationRoot } of containers) {
const inventory = await artifactCliEvidence("inspect-artifact", target);
const input = join(root, `${format}-projection.json`);
await writeFile(input, JSON.stringify({ inventory_evidence: [inventory] }));
const evidence = await artifactCliEvidence(
"project-apple-application-graph",
input,
);
const projection = evidence.normalized_result;
assert.equal(evidence.subject?.format, format);
assert.equal(projection.root_format, format);
assert.deepEqual(projection.platforms, ["macos"]);
assert.deepEqual(projection.application_roots, [applicationRoot]);
assert.equal(projection.coverage.status, "complete-within-inventory");
const observed = anatomy(projection, applicationRoot);
assert.deepEqual(
observed.bundles.map(({ path, role, layout, parent }) => [
path,
role,
layout,
parent,
]),
EXPECTED_BUNDLES,
`${format} bundle roles drifted`,
);
assert.deepEqual(observed.privileged_helpers, [
"Contents/Library/LaunchServices/com.example.rea.helper",
]);
assert.deepEqual(observed.launchd_plists, [
"agent:Contents/Library/LaunchAgents/com.example.rea.agent.plist",
"daemon:Contents/Library/LaunchDaemons/com.example.rea.helper.plist",
]);
assert.deepEqual(observed.helpers, ["Contents/Helpers/rea-tool"]);
assert.ok(
projection.symlinks.some((path) =>
path.endsWith(`${FRAMEWORK}/Versions/Current`),
),
"Versions/Current symlink was not reported",
);
anatomies.push(observed);
if (format === "directory")
await withArtifactMcp(app, async (client) => {
assert.deepEqual(
await artifactMcpResult(client, "project_apple_application_graph", {
inventory_evidence: [inventory],
}),
projection,
);
});
}
assert.deepEqual(anatomies[1], anatomies[0], "ZIP anatomy differs");
assert.deepEqual(anatomies[2], anatomies[0], "DMG anatomy differs");
const { stdout: attached } = await exec("/usr/bin/hdiutil", ["info"]);
assert.ok(!attached.includes(dmg), "DMG remained attached after inventory");
process.stdout.write(
`${JSON.stringify({
ok: true,
mocked: false,
cli: true,
stdio_mcp: true,
containers: containers.map(({ format }) => format),
bundles: EXPECTED_BUNDLES.length,
dmg_detached: true,
})}\n`,
);
} finally {
await rm(root, { recursive: true, force: true });
}
@@ -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) => {
+1 -1
View File
@@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
metadata:
version: "26"
tool_count: 131
catalog_digest: "e893b9df15ed6eec249c8f9adb583dc4870c240f9126f8e42680a014cd6fba90"
catalog_digest: "44b30e8892c84cf27766d5739228268b8a85f885c63002157bc134ed8d636bb8"
---
# REA
@@ -69,6 +69,14 @@ ASAR, or DMG inputs when the artifact graph and findings help answer the
question. It returns the complete artifact graph inline. Cite graph manifest
IDs when using them.
For an IPA or a macOS `.app`, ZIP, or DMG, pass the inventory Evidence to
`project_apple_application_graph`. It reports application roots and nested
bundles by path convention: app extensions, XPC services, frameworks, login
items, system and driver extensions, and plug-ins. It also lists privileged
helpers, launchd plists, symlinks, and each bundle's `info_plist_path` and
executable candidates. Roles are path conventions, not parsed plists; read the
listed plists with `inspect_plist`.
Use `extract_artifact` when materialized files are needed. It takes no arguments
and materializes all regular files into a fresh temporary directory chosen by
REA. Symlinks and encrypted entries are inventory facts, not extractable files.
+42 -15
View File
@@ -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);
+1 -1
View File
@@ -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";
+186
View File
@@ -0,0 +1,186 @@
import { execFile } from "node:child_process";
import { lstat, readdir, readFile, realpath, stat } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { z } from "zod";
import { BinaryTargetError } from "../domain/configurationErrors.js";
import { isPathWithinRoot } from "../domain/localPath.js";
import { parseXmlPropertyList } from "../domain/propertyListKeys.js";
import { err, ok, type Result } from "../domain/result.js";
const execFileAsync = promisify(execFile);
/** Where one app bundle layout keeps its Info.plist and program file. */
interface BundleLayout {
readonly plist: readonly string[];
readonly programs: readonly string[];
readonly programsLabel: string;
}
const BUNDLE_LAYOUTS: readonly BundleLayout[] = [
// macOS bundles keep both under Contents.
{
plist: ["Contents", "Info.plist"],
programs: ["Contents", "MacOS"],
programsLabel: "Contents/MacOS",
},
// iOS-style bundles, as extracted from an IPA, keep both at the root.
{ plist: ["Info.plist"], programs: [], programsLabel: "the bundle root" },
];
/** A resolved bundle program file and the Info.plist that declared it. */
export interface ResolvedAppBundle {
readonly executable: string;
readonly infoPlist?: string;
}
/**
* Resolve an app bundle directory to its declared program file. macOS and
* flat iOS-style layouts are read directly. An iOS app installed on a Mac is
* a wrapper whose `Wrapper` directory holds the iOS bundle; that bundle is
* found by listing `Wrapper`, never by following the `WrappedBundle` link.
*/
export const resolveAppBundleExecutable = async (
path: string,
): Promise<Result<ResolvedAppBundle, BinaryTargetError>> => {
const layout = await presentLayout(path);
if (layout !== undefined) return resolveLayoutExecutable(path, layout);
const wrapped = await wrappedBundle(path);
if (wrapped !== undefined) {
const wrappedLayout = await presentLayout(wrapped);
if (wrappedLayout !== undefined)
return resolveLayoutExecutable(wrapped, wrappedLayout);
}
return err(
new BinaryTargetError(path, "app has no readable CFBundleExecutable", {
cause: new Error(
"No Contents/Info.plist, root Info.plist, or single Wrapper/*.app bundle was found",
),
}),
);
};
const presentLayout = async (
bundle: string,
): Promise<BundleLayout | undefined> => {
for (const layout of BUNDLE_LAYOUTS)
if (await layoutPlistPresent(join(bundle, ...layout.plist))) return layout;
return undefined;
};
/**
* Whether a layout's Info.plist exists. Readable symlinks count, as they did
* when the plist was read directly; any failure other than absence, such as
* a permission denial, is left for the caller to report.
*/
const layoutPlistPresent = async (path: string): Promise<boolean> => {
try {
return (await stat(path)).isFile();
} catch (cause: unknown) {
if (isAbsence(cause)) return false;
throw cause;
}
};
const isAbsence = (cause: unknown): boolean => {
const code: unknown =
cause instanceof Error ? Reflect.get(cause, "code") : undefined;
return code === "ENOENT" || code === "ENOTDIR";
};
/** The single real `.app` directory inside an iOS-on-Mac `Wrapper`. */
const wrappedBundle = async (bundle: string): Promise<string | undefined> => {
const wrapper = join(bundle, "Wrapper");
try {
if (!(await lstat(wrapper)).isDirectory()) return undefined;
} catch (cause: unknown) {
if (isAbsence(cause)) return undefined;
throw cause;
}
const apps = (await readdir(wrapper, { withFileTypes: true })).filter(
(entry) => entry.isDirectory() && entry.name.toLowerCase().endsWith(".app"),
);
const [app] = apps;
return apps.length === 1 && app !== undefined
? join(wrapper, app.name)
: undefined;
};
const resolveLayoutExecutable = async (
bundle: string,
layout: BundleLayout,
): Promise<Result<ResolvedAppBundle, BinaryTargetError>> => {
const plistPath = join(bundle, ...layout.plist);
let name: string;
try {
const plist = await readFile(plistPath);
name =
plist.subarray(0, 6).toString("ascii") === "bplist"
? await readBinaryPlistExecutable(plistPath)
: parseXmlPlistExecutable(plist.toString("utf8"));
} catch (cause: unknown) {
return err(
new BinaryTargetError(bundle, "app has no readable CFBundleExecutable", {
cause,
}),
);
}
if (!isSafeExecutableName(name))
return err(
new BinaryTargetError(bundle, "app has an unsafe CFBundleExecutable"),
);
const programs = join(bundle, ...layout.programs);
const executable = join(programs, name);
try {
const [canonicalPrograms, canonicalExecutable] = await Promise.all([
realpath(programs),
realpath(executable),
]);
if (!isPathWithinRoot(canonicalPrograms, canonicalExecutable))
return err(
new BinaryTargetError(
bundle,
`app program file leaves ${layout.programsLabel}`,
),
);
return ok({ executable: canonicalExecutable, infoPlist: plistPath });
} catch (cause: unknown) {
return err(
new BinaryTargetError(bundle, "app program file is missing", { cause }),
);
}
};
/** Decode the top-level executable name with an XML parser, not a pattern. */
const parseXmlPlistExecutable = (plist: string): string => {
// An unrelated `__proto__` entry must not make the bundle unreadable.
const { value } = parseXmlPropertyList(plist);
const executable = executableEntrySchema.safeParse(value);
if (!executable.success) throw new Error("CFBundleExecutable is missing");
return executable.data.CFBundleExecutable;
};
const executableEntrySchema = z.looseObject({ CFBundleExecutable: z.string() });
const readBinaryPlistExecutable = async (plistPath: string): Promise<string> =>
// `-n` strips only the newline plutil appends; it requires macOS 12+.
(
await execFileAsync("/usr/bin/plutil", [
"-extract",
"CFBundleExecutable",
"raw",
"-n",
"-o",
"-",
plistPath,
])
).stdout;
const isSafeExecutableName = (name: string): boolean =>
name.length > 0 &&
name !== "." &&
name !== ".." &&
!name.includes("\0") &&
!/[/\\]/u.test(name);
+13 -82
View File
@@ -3,22 +3,19 @@ import { createHash } from "node:crypto";
import {
access,
open,
readFile,
realpath,
stat,
type FileHandle,
} from "node:fs/promises";
import { execFile } from "node:child_process";
import { extname, isAbsolute, join, resolve } from "node:path";
import { promisify } from "node:util";
import { extname, isAbsolute, resolve } from "node:path";
import { z } from "zod";
import { isPathWithinRoot } from "../domain/localPath.js";
import { parseXmlPropertyList } from "../domain/propertyListKeys.js";
import { BinaryTargetError } from "../domain/configurationErrors.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { err, ok, type Result } from "../domain/result.js";
import {
resolveAppBundleExecutable,
type ResolvedAppBundle,
} from "./AppBundleExecutable.js";
import {
validateDosComLength,
type ExecutableFormatHint,
@@ -35,8 +32,6 @@ import {
type ExecutableMetadata,
} from "../domain/binaryTarget.js";
const execFileAsync = promisify(execFile);
/**
* Resolve and classify a readable local target before a provider is selected.
* FAT Mach-O inputs select only a host-compatible architecture so setup remains
@@ -55,7 +50,9 @@ export const parseBinaryTarget = async (
const canonical = await realpath(candidate);
const resolved = await resolveAppBundle(canonical);
if (!resolved.ok) return err(resolved.error);
const path = resolved.value;
const { executable: path, infoPlist } = resolved.value;
const bundle =
infoPlist === undefined ? {} : { bundleInfoPlist: infoPlist };
const handle = await open(path, "r");
try {
if (!(await handle.stat()).isFile())
@@ -107,6 +104,7 @@ export const parseBinaryTarget = async (
return ok({
path,
sourcePath: process.platform === "win32" ? candidate : canonical,
...bundle,
sha256: await sha256Handle(handle),
kind: "executable",
...detected.value,
@@ -216,89 +214,22 @@ const sha256Handle = async (handle: FileHandle): Promise<string> => {
const resolveAppBundle = async (
path: string,
): Promise<Result<string, BinaryTargetError>> => {
): Promise<Result<ResolvedAppBundle, BinaryTargetError>> => {
const metadata = await stat(path);
if (!metadata.isDirectory()) return ok(path);
if (!metadata.isDirectory()) return ok({ executable: path });
if (extname(path).toLowerCase() !== ".app")
return err(
new BinaryTargetError(
path,
"this opening route requires a file or macOS app bundle",
"this opening route requires a file or app bundle",
{
constraint: "directory_requires_file",
},
),
);
const plistPath = join(path, "Contents", "Info.plist");
let name: string;
try {
const plist = await readFile(plistPath);
name =
plist.subarray(0, 6).toString("ascii") === "bplist"
? await readBinaryPlistExecutable(plistPath)
: parseXmlPlistExecutable(plist.toString("utf8"));
} catch (cause: unknown) {
return err(
new BinaryTargetError(path, "app has no readable CFBundleExecutable", {
cause,
}),
);
}
if (!isSafeExecutableName(name))
return err(
new BinaryTargetError(path, "app has an unsafe CFBundleExecutable"),
);
const programs = join(path, "Contents", "MacOS");
const executable = join(programs, name);
try {
const [canonicalPrograms, canonicalExecutable] = await Promise.all([
realpath(programs),
realpath(executable),
]);
if (!isPathWithinRoot(canonicalPrograms, canonicalExecutable))
return err(
new BinaryTargetError(path, "app program file leaves Contents/MacOS"),
);
return ok(canonicalExecutable);
} catch (cause: unknown) {
return err(
new BinaryTargetError(path, "app program file is missing", { cause }),
);
}
return resolveAppBundleExecutable(path);
};
/** Decode the top-level executable name with an XML parser, not a pattern. */
const parseXmlPlistExecutable = (plist: string): string => {
// An unrelated `__proto__` entry must not make the bundle unreadable.
const { value } = parseXmlPropertyList(plist);
const executable = executableEntrySchema.safeParse(value);
if (!executable.success) throw new Error("CFBundleExecutable is missing");
return executable.data.CFBundleExecutable;
};
const executableEntrySchema = z.looseObject({ CFBundleExecutable: z.string() });
const readBinaryPlistExecutable = async (plistPath: string): Promise<string> =>
// `-n` strips only the newline plutil appends; it requires macOS 12+.
(
await execFileAsync("/usr/bin/plutil", [
"-extract",
"CFBundleExecutable",
"raw",
"-n",
"-o",
"-",
plistPath,
])
).stdout;
const isSafeExecutableName = (name: string): boolean =>
name.length > 0 &&
name !== "." &&
name !== ".." &&
!name.includes("\0") &&
!/[/\\]/u.test(name);
const readExecutableMetadata = async (
handle: FileHandle,
hostArchitecture: NodeJS.Architecture,
+1 -1
View File
@@ -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";
+15
View File
@@ -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:
+9 -3
View File
@@ -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);
+1 -1
View File
@@ -3,7 +3,7 @@ import { z } from "zod";
import type { AnalysisOperationPort } from "./AnalysisProvider.js";
import type { EnhancedResult } from "./EnhancedToolTypes.js";
import type { enhancedInputSchemas } from "../contracts/enhancedInputs.js";
import { interfaceBuilderAnalysisSchema } from "../domain/interfaceBuilderGraph.js";
import { interfaceBuilderAnalysisSchema } from "../domain/apple/interfaceBuilderGraph.js";
import {
nativeInvestigationGraphSchema,
nativeInvestigationTraceSchema,
+5
View File
@@ -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);
});
+12 -2
View File
@@ -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 =
+25 -1
View File
@@ -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 {
+4 -1
View File
@@ -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 &&
+8
View File
@@ -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[]>;
@@ -1,16 +1,16 @@
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { Evidence } from "../domain/evidence.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { Evidence } from "../../domain/evidence.js";
import {
appleApplicationProjectionInputSchema,
projectAppleApplication,
} from "../domain/appleApplication.js";
import type { Result } from "../domain/result.js";
import { APPLE_APPLICATION_PROVIDER } from "./InvestigationProviders.js";
import { projectInventoryEvidence } from "./InventoryProjectionEvidence.js";
} from "../../domain/apple/appleApplication.js";
import type { Result } from "../../domain/result.js";
import { APPLE_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
import { projectInventoryEvidence } from "../InventoryProjectionEvidence.js";
const OPERATION = "project_apple_application_graph" as const;
/** Project authenticated IPA inventory Evidence into Apple application evidence. */
/** Project authenticated IPA or macOS app inventory Evidence into Apple application evidence. */
export const projectAppleApplicationEvidence = (
rawInput: unknown,
): Result<Evidence, AnalysisError> => {
@@ -21,7 +21,7 @@ export const projectAppleApplicationEvidence = (
operation: OPERATION,
predicateType: "rea.apple-application-graph",
provider: APPLE_APPLICATION_PROVIDER,
subjectFormat: () => "ipa",
subjectFormat: (first) => first.subject?.format ?? "unknown",
protocolError: "Apple application projection produced an invalid result",
});
};
@@ -132,7 +132,7 @@ export const bindExecutionTarget = (
if (
result.value.subject !== null &&
result.value.subject.sha256 !== target.sha256 &&
!isArtifactInventorySubject(operation, result.value.subject, target)
!isArtifactBundleSubject(operation, result.value.subject, target)
)
return err(
new ProviderAdapterError(
@@ -143,10 +143,12 @@ export const bindExecutionTarget = (
return ok({ ...result.value, subject: result.value.subject ?? subject });
};
const isArtifactInventorySubject = (
const isArtifactBundleSubject = (
operation: AnalysisOperation,
subject: EvidenceSubjectTarget,
target: BinaryTarget,
): boolean =>
(operation === "inventory_artifact" || operation === "inspect_artifact") &&
(operation === "inventory_artifact" ||
operation === "inspect_artifact" ||
operation === "extract_artifact") &&
subject.path === (target.sourcePath ?? target.path);
@@ -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 {
@@ -268,11 +268,12 @@ const addSourceMapEdge = (
context: JavaScriptArtifactGraphContext,
input: FindingInput<StaticSourceMap>,
): void => {
const resolvedPath = resolveArtifactPath(
input.value.declared_url,
input.file.path,
context.filesByPath,
);
const resolvedPath = resolveArtifactPathByContext({
declaredPath: input.value.declared_url,
sourcePath: input.file.path,
context: "url-reference",
files: context.filesByPath,
}).resolved_path;
const resolvedFile =
resolvedPath === null ? undefined : context.filesByPath.get(resolvedPath);
const resolvedNode =
@@ -341,17 +342,26 @@ const resolveReference = (
? null
: { path: `${sourcePath}#module:${specifier}`, file, node: module };
}
const path = resolveArtifactPath(
specifier,
sourcePath,
context.filesByPath,
reference.kind === "require"
? "require"
: reference.kind === "static-import" ||
reference.kind === "dynamic-import"
? "import"
: undefined,
);
// Worker scripts are URLs relative to the script, not module specifiers.
const path =
reference.kind === "worker" || reference.kind === "service-worker"
? resolveArtifactPathByContext({
declaredPath: specifier,
sourcePath,
context: "url-reference",
files: context.filesByPath,
}).resolved_path
: resolveArtifactPath(
specifier,
sourcePath,
context.filesByPath,
reference.kind === "require"
? "require"
: reference.kind === "static-import" ||
reference.kind === "dynamic-import"
? "import"
: undefined,
);
if (path === null) return null;
const file = context.filesByPath.get(path);
const node = context.fileNodes.get(path);
@@ -562,6 +562,61 @@ describe("contextual JavaScript module identity", () => {
});
});
describe("url-reference JavaScript artifact path resolution", () => {
it("resolves sibling paths and rejects external URL forms", () => {
const files = fileMap([
file("dist/app.js", "root"),
file("dist/app.js.map", "root"),
file("dist/worker.js", "root"),
]);
expect(
resolve({
declaredPath: "app.js.map",
sourcePath: "dist/app.js",
context: "url-reference",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "dist/app.js.map",
});
expect(
resolve({
declaredPath: "app.js.map?v=1#section",
sourcePath: "dist/app.js",
context: "url-reference",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "dist/app.js.map",
});
expect(
resolve({
declaredPath: "//cdn.example.com/app.js.map",
sourcePath: "dist/app.js",
context: "url-reference",
files,
}),
).toMatchObject({
resolution_status: "external",
resolved_path: null,
});
expect(
resolve({
declaredPath: "https://cdn.example.com/app.js.map",
sourcePath: "dist/app.js",
context: "url-reference",
files,
}),
).toMatchObject({
resolution_status: "external",
resolved_path: null,
});
});
});
const resolve = (input: ResolveArtifactPathInput) =>
resolveArtifactPathByContext(input);
@@ -13,7 +13,8 @@ type ArtifactPathResolutionContext =
| "package-entrypoint"
| "filesystem-expression"
| "module-specifier"
| "html-reference";
| "html-reference"
| "url-reference";
type UnresolvedArtifactPathStatus =
| "not-found"
@@ -127,6 +128,7 @@ const contextualCandidate = (
const { context } = input;
const declared =
context === "html-reference" ||
context === "url-reference" ||
(context === "module-specifier" && input.moduleKind !== "require")
? stripQueryAndFragment(input.declaredPath)
: input.declaredPath;
@@ -140,9 +142,9 @@ const contextualCandidate = (
]);
if (!declared.startsWith(".") && !declared.startsWith("/"))
return bareModuleCandidate(input, declared);
} else if (hasScheme(declared))
} else if (looksExternal(declared))
return unresolvedOutcome(input, "external", [
"URL schemes are outside this local artifact path context.",
"URL schemes and protocol-relative URLs are outside this local artifact path context.",
]);
const relative = declared.startsWith("/") ? declared.slice(1) : declared;
return declared.startsWith("/")
@@ -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;
+4 -4
View File
@@ -7,12 +7,12 @@ import {
type ProviderIdentity,
type ExecutionOptions,
} from "../application/AnalysisProvider.js";
import { inspectBundleKeyedArchive } from "./KeyedArchiveReader.js";
import { inspectBundleKeyedArchive } from "./apple/KeyedArchiveReader.js";
import { basename, dirname } from "node:path";
import { inventoryArtifact } from "../application/ArtifactInventory.js";
import { extractArtifact } from "../application/ArtifactExtraction.js";
import { analyzeInterfaceBuilderBundle } from "../application/InterfaceBuilderAnalysis.js";
import { analyzeAppleAssetCatalogs } from "../application/AppleAssetCatalogAnalysis.js";
import { analyzeInterfaceBuilderBundle } from "./apple/InterfaceBuilderAnalysis.js";
import { analyzeAppleAssetCatalogs } from "./apple/AppleAssetCatalogAnalysis.js";
import {
ARTIFACT_ANALYSIS_OPERATIONS,
artifactInventoryInputSchema,
@@ -24,7 +24,7 @@ import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.
import { ArtifactOperationError } from "../domain/artifactOperationError.js";
import { type AnalysisError } from "../domain/analysisErrorBase.js";
import type { JsonValue } from "../domain/jsonValue.js";
import { interfaceBuilderLimitsSchema } from "../domain/interfaceBuilderGraph.js";
import { interfaceBuilderLimitsSchema } from "../domain/apple/interfaceBuilderGraph.js";
import { err, ok } from "../domain/result.js";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
import {
+23 -8
View File
@@ -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" &&
+1 -1
View File
@@ -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();
+32 -1
View File
@@ -23,6 +23,13 @@ const attachOutputSchema = z.object({
}),
),
});
const infoOutputSchema = z.object({
images: z.array(
z.object({
"system-entities": z.array(z.object({ "dev-entry": z.string() })),
}),
),
});
/** Narrow host seam for tested, shell-free hdiutil lifecycle operations. */
export interface NativeDmgHost {
@@ -113,7 +120,10 @@ export class NativeDmgArtifactReader implements ArtifactReader {
});
this.#provenance.push(command(["detach", device], ["mount"]));
} catch (cause: unknown) {
detachFailure ??= cause;
// Detaching a synthesized APFS container also ejects the image that
// backs it, so a later whole disk of the same image may already be
// gone. Only a device that is still attached is a cleanup failure.
if (await this.#isAttached(device)) detachFailure ??= cause;
}
}
this.#devices = [];
@@ -131,6 +141,27 @@ export class NativeDmgArtifactReader implements ArtifactReader {
);
}
/**
* Report whether hdiutil still lists a device; unknown state counts as
* attached. Like detach, this cleanup query runs after the inventory
* snapshot has captured provenance, so it is not recorded there.
*/
async #isAttached(device: string): Promise<boolean> {
try {
const info = await runChecked(this.host, ["info", "-plist"], undefined, {
timeoutMs: DETACH_TIMEOUT_MS,
});
const parsed = infoOutputSchema.parse(parse(info.stdout));
return parsed.images.some((image) =>
image["system-entities"].some(
(entity) => entity["dev-entry"] === device,
),
);
} catch {
return true;
}
}
async attach(signal?: AbortSignal): Promise<void> {
await runChecked(this.host, ["verify", this.path], signal);
this.#provenance.push(command(["verify", this.path], ["read"]));
@@ -1,14 +1,14 @@
import { createHash } from "node:crypto";
import { lstat } from "node:fs/promises";
import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
import { ArtifactReaderFailure } from "../ArtifactReader.js";
import { analyzeInterfaceBuilderBundle } from "./InterfaceBuilderAnalysis.js";
import {
appleAssetCatalogInputSchema,
parseAppleAssetCatalogRecords,
projectAppleAssetCatalogPage,
} from "../domain/appleAssetCatalog.js";
import { execFileOutput } from "../process/ExecFileOutput.js";
} from "../../domain/apple/appleAssetCatalog.js";
import { execFileOutput } from "../../process/ExecFileOutput.js";
const ASSETUTIL = "/usr/bin/assetutil";
const MAX_CATALOG_BYTES = 64 * 1024 * 1024;
@@ -105,7 +105,7 @@ export const analyzeAppleAssetCatalogs = async (input: {
const inspectAssetCatalogEntry = async (input: {
readonly reader: DirectoryArtifactReader;
readonly entry: import("../artifacts/ArtifactReader.js").ArtifactEntry;
readonly entry: import("../ArtifactReader.js").ArtifactEntry;
readonly runAssetUtil: (
path: string,
signal?: AbortSignal,
@@ -237,7 +237,7 @@ const runAssetUtil = async (
const digestBounded = async (
reader: DirectoryArtifactReader,
entry: import("../artifacts/ArtifactReader.js").ArtifactEntry,
entry: import("../ArtifactReader.js").ArtifactEntry,
signal?: AbortSignal,
): Promise<string> => {
if (entry.declaredSize === null || entry.declaredSize > MAX_CATALOG_BYTES)
@@ -1,27 +1,25 @@
import { createHash } from "node:crypto";
import { parseBinary } from "plist";
import { DirectoryArtifactReader } from "../artifacts/DirectoryArtifactReader.js";
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
import { projectPlistValue } from "../../domain/apple/plistValue.js";
import {
omittedPrototypeKeysLimitation,
parseXmlPropertyList,
} from "../domain/propertyListKeys.js";
import {
decodeNibArchive,
type NibArchiveDocument,
} from "../artifacts/NibArchive.js";
import type { ArtifactEntry } from "../artifacts/ArtifactReader.js";
} from "../../domain/propertyListKeys.js";
import { decodeNibArchive, type NibArchiveDocument } from "./NibArchive.js";
import type { ArtifactEntry } from "../ArtifactReader.js";
import {
buildInterfaceBuilderAnalysis,
interfaceBuilderLimitsSchema,
type InterfaceBuilderDocumentInput,
} from "../domain/interfaceBuilderGraph.js";
} from "../../domain/apple/interfaceBuilderGraph.js";
import {
mergeNibHierarchies,
projectNibViewHierarchy,
type NibHierarchyNode,
} from "./NibViewHierarchy.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import { jsonValueSchema, type JsonValue } from "../../domain/jsonValue.js";
const MAX_DOCUMENT_BYTES = 64 * 1024 * 1024;
@@ -214,15 +212,17 @@ const projectNibArchive = (archive: NibArchiveDocument) => {
const object = byId.get(objectId);
if (object === undefined) return null;
const storedClass = object.class_name.replace(/\0+$/u, "");
return storedClass === "NSClassSwapper"
? (referencedString(object.values.NSClassName) ?? storedClass)
if (storedClass === "NSClassSwapper")
return referencedString(object.values.NSClassName) ?? storedClass;
return storedClass === "UIClassSwapper"
? (referencedString(object.values.UIClassName) ?? storedClass)
: storedClass;
};
const objects = Object.fromEntries(
archive.objects
.filter((object) => {
const name = runtimeClass(object.id) ?? "";
return !/^(?:NSObject|NSIBObjectData|NSString|NSMutableString|NSNumber|NSArray|NSMutableArray|NSSet|NSMutableSet|NSDictionary|NSMutableDictionary|NSApplication|NSNib.*Connector)$/u.test(
return !/^(?:NSObject|NSIBObjectData|NSString|NSMutableString|NSNumber|NSArray|NSMutableArray|NSSet|NSMutableSet|NSDictionary|NSMutableDictionary|NSApplication|NSNib.*Connector|UIRuntime\w*Connection)$/u.test(
name,
);
})
@@ -268,26 +268,34 @@ const projectNibArchive = (archive: NibArchiveDocument) => {
};
for (const object of archive.objects) {
const className = object.class_name.replace(/\0+$/u, "");
const source = reference(object.values.NSSource);
const destination = reference(object.values.NSDestination);
if (className.includes("Connector") && source !== null) {
const label = referencedString(object.values.NSLabel);
const type = className.includes("Outlet")
? "outlet"
: className.includes("Control")
? "action"
: className;
// NSSource is the outlet owner or the sending control; NSDestination is
// the outlet value or the action target, and a nil target is the first
// responder.
(connections[String(source)] ??= []).push({
type,
"destination-id": destination === null ? null : String(destination),
label,
source_id: String(source),
archive_object_id: String(object.id),
});
}
// AppKit connectors use NS-prefixed keys; UIKit's runtime outlet and
// event connections use UI-prefixed keys with the same roles.
const uiKit = /^UIRuntime\w*Connection$/u.test(className);
if (!uiKit && !className.includes("Connector")) continue;
const values = object.values;
const source = reference(uiKit ? values.UISource : values.NSSource);
if (source === null) continue;
const destination = reference(
uiKit ? values.UIDestination : values.NSDestination,
);
const label = referencedString(uiKit ? values.UILabel : values.NSLabel);
const type = className.includes("Outlet")
? "outlet"
: className.includes("Control") || className.includes("Event")
? "action"
: className;
const eventMask = values.UIEventMask;
// The source is the outlet owner or the sending control; the destination
// is the outlet value or the action target, and a nil target is the
// first responder.
(connections[String(source)] ??= []).push({
type,
"destination-id": destination === null ? null : String(destination),
label,
source_id: String(source),
archive_object_id: String(object.id),
...(typeof eventMask === "number" ? { ui_event_mask: eventMask } : {}),
});
}
let hierarchyOmitted = 0;
const hierarchyFor = (
@@ -390,6 +398,7 @@ const readEntry = async (
return Buffer.concat(chunks, length);
};
/** Project decoded plist data and dates while retaining omitted-key coverage. */
const decodePlist = (
bytes: Buffer,
): { readonly value: JsonValue; readonly omittedPrototypeKeys: number } => {
@@ -397,5 +406,5 @@ const decodePlist = (
bytes.subarray(0, 8).toString("ascii") === "bplist00"
? { value: parseBinary(bytes), omittedPrototypeKeys: 0 }
: parseXmlPropertyList(bytes.toString("utf8"));
return { value: jsonValueSchema.parse(value), omittedPrototypeKeys };
return { value: projectPlistValue(value).value, omittedPrototypeKeys };
};
@@ -2,19 +2,19 @@ import { createHash } from "node:crypto";
import { TextDecoder } from "node:util";
import { parseBinary } from "plist";
import { z } from "zod";
import type { JsonValue } from "../domain/jsonValue.js";
import { projectPlistValue } from "../domain/plistValue.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { projectPlistValue } from "../../domain/apple/plistValue.js";
import {
omittedPrototypeKeysLimitation,
parseXmlPropertyList,
} from "../domain/propertyListKeys.js";
} from "../../domain/propertyListKeys.js";
import {
keyedArchiveInputSchema,
keyedArchiveResultSchema,
projectKeyedArchive,
} from "../domain/keyedArchive.js";
import { DirectoryArtifactReader } from "./DirectoryArtifactReader.js";
import { ArtifactReaderFailure } from "./ArtifactReader.js";
} from "../../domain/apple/keyedArchive.js";
import { DirectoryArtifactReader } from "../DirectoryArtifactReader.js";
import { ArtifactReaderFailure } from "../ArtifactReader.js";
const MAX_BYTES = 64 * 1024 * 1024;
@@ -1,4 +1,4 @@
import type { JsonValue } from "../domain/jsonValue.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { TextDecoder } from "node:util";
const MAGIC = Buffer.from("NIBArchive", "ascii");
@@ -1,5 +1,5 @@
import type { NibArchiveDocument } from "../artifacts/NibArchive.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { NibArchiveDocument } from "./NibArchive.js";
import type { JsonValue } from "../../domain/jsonValue.js";
export type NibHierarchyNode = {
objectID: string;
+3 -3
View File
@@ -2,7 +2,7 @@ import { reconstructionCoverageEvaluationInputSchema } from "../domain/reconstru
import { Cli, z } from "incur";
import { projectAndroidApplicationEvidence } from "../application/android/AndroidApplicationService.js";
import { projectAppleApplicationEvidence } from "../application/AppleApplicationService.js";
import { projectAppleApplicationEvidence } from "../application/apple/AppleApplicationService.js";
import {
compareApplicationVersionsEvidenceValidated,
compareJavaScriptExportShapesEvidenceValidated,
@@ -19,7 +19,7 @@ import { AnalysisInputError } from "../domain/analysisErrorCore.js";
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
import { type AnalysisError } from "../domain/analysisErrorBase.js";
import { androidApplicationProjectionInputSchema } from "../domain/android/androidApplication.js";
import { appleApplicationProjectionInputSchema } from "../domain/appleApplication.js";
import { appleApplicationProjectionInputSchema } from "../domain/apple/appleApplication.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import type { Logger } from "../logger.js";
import { traceApplicationFeatureInputSchema } from "../domain/javascript/javascriptFeatureTraceSchemas.js";
@@ -103,7 +103,7 @@ export const registerApplicationCommands = (
logger,
name: CLI_COMMANDS.projectAppleApplicationGraph,
description:
"Project authenticated IPA inventory Evidence into an Apple application graph",
"Project authenticated IPA or macOS app inventory Evidence into an Apple application graph",
inputSchema: appleApplicationProjectionInputSchema,
workflow: (input) => {
const result = projectAppleApplicationEvidence(input);
+3 -3
View File
@@ -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 -1
View File
@@ -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 -2
View File
@@ -23,7 +23,7 @@ import {
import {
appleApplicationProjectionInputSchema,
appleApplicationProjectionResultSchema,
} from "../domain/appleApplication.js";
} from "../domain/apple/appleApplication.js";
import type { ToolContract } from "./toolContracts.js";
import { toolContractMetadata } from "./toolEffects.js";
import { evidenceResultOf } from "./toolOutputSchemas.js";
@@ -36,6 +36,7 @@ import {
import {
ANDROID_APPLICATION_GRAPH_EXAMPLE,
APPLE_APPLICATION_GRAPH_EXAMPLE,
MACOS_APPLICATION_GRAPH_EXAMPLE,
} from "./mobileApplicationGraphExamples.js";
const traceOutputSchema = evidenceResultOf(applicationFeatureTraceResultSchema);
@@ -278,7 +279,7 @@ export const APPLICATION_TOOL_CONTRACTS = [
name: "project_apple_application_graph",
...toolContractMetadata("project_apple_application_graph"),
description:
"Project authenticated IPA inventory_artifact Evidence into an execution-free Apple application inventory. Reports exact component paths and hashes, runtime-family hints, and path-based bridge hypotheses without parsing plist/CMS semantics or claiming observed runtime calls.",
"Project authenticated IPA, macOS .app directory, ZIP, or DMG inventory Evidence into an execution-free Apple application inventory. Reports application roots, nested bundles with path-convention roles (app extensions, XPC services, login items, privileged helpers, system extensions, frameworks), launchd plists, exact component paths and hashes, runtime-family hints, and path-based bridge hypotheses without parsing plist/CMS semantics or claiming observed runtime calls.",
kind: "application",
inputSchema: appleApplicationProjectionInputSchema,
outputSchema: evidenceResultOf(appleApplicationProjectionResultSchema),
@@ -287,6 +288,11 @@ export const APPLICATION_TOOL_CONTRACTS = [
title: "Project IPA inventory Evidence into an Apple application graph",
input: APPLE_APPLICATION_GRAPH_EXAMPLE,
},
{
title:
"Project a macOS .app directory inventory into its bundle anatomy",
input: MACOS_APPLICATION_GRAPH_EXAMPLE,
},
],
},
] as const satisfies readonly ToolContract[];
+2 -2
View File
@@ -5,8 +5,8 @@ import { artifactOutputSchemas } from "./toolOutputSchemas.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { toolContractMetadata } from "./toolEffects.js";
import { requireOutputSchema } from "./toolOutputSchemaPrimitives.js";
import { appleAssetCatalogInputSchema } from "../domain/appleAssetCatalog.js";
import { keyedArchiveInputSchema } from "../domain/keyedArchive.js";
import { appleAssetCatalogInputSchema } from "../domain/apple/appleAssetCatalog.js";
import { keyedArchiveInputSchema } from "../domain/apple/keyedArchive.js";
import { artifactIntegrityPolicySchema } from "../domain/artifactGraph.js";
/** Exact caller boundary for deterministic artifact inventory. */
export const artifactInventoryInputSchema = z.strictObject({
+2 -2
View File
@@ -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
@@ -2,7 +2,7 @@ import { canonicalDigest } from "../domain/comparisonSemantics.js";
import { createEvidence } from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
const inventory = (format: "apk" | "ipa", digit: string) => {
const inventory = (format: "apk" | "ipa" | "directory", digit: string) => {
const sha = digit.repeat(64);
const artifactId = `art_${canonicalDigest({ sha256: sha }, "Artifact inventory")}`;
const occurrenceId = `occ_${canonicalDigest(
@@ -112,3 +112,20 @@ export const APPLE_APPLICATION_GRAPH_EXAMPLE = {
),
],
};
/** Catalog example for a macOS application bundle inventoried as a directory. */
export const MACOS_APPLICATION_GRAPH_EXAMPLE = {
inventory_evidence: [
createEvidence(
{ path: "Fixture.app", sha256: "c".repeat(64), format: "directory" },
provider,
{
operation: "inventory_artifact",
parameters: {},
result: inventory("directory", "c"),
confidence: "observed",
authority: "shipped-artifact",
},
),
],
};
+1 -1
View File
@@ -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(
+2 -2
View File
@@ -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";
+2 -2
View File
@@ -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";
+8 -8
View File
@@ -30,18 +30,18 @@ import {
} from "../domain/nativeInspection.js";
import { artifactExtractionResultSchema } from "../domain/artifactGraph.js";
import { artifactInspectionResultSchema } from "../domain/artifactInspection.js";
import { interfaceBuilderAnalysisSchema } from "../domain/interfaceBuilderGraph.js";
import { keyedArchiveResultSchema } from "../domain/keyedArchive.js";
import { appleAssetCatalogResultSchema } from "../domain/appleAssetCatalog.js";
import { interfaceBuilderAnalysisSchema } from "../domain/apple/interfaceBuilderGraph.js";
import { keyedArchiveResultSchema } from "../domain/apple/keyedArchive.js";
import { appleAssetCatalogResultSchema } from "../domain/apple/appleAssetCatalog.js";
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";

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