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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -45,3 +45,6 @@ Thumbs.db
|
||||
|
||||
# Local reference sources and analysis artifacts
|
||||
/_reference/
|
||||
|
||||
# Generated website download
|
||||
/website/public/examples/notes-example.zip
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
[Documentation](https://morluto.github.io/rea/) · [Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
|
||||
**[Website](https://morluto.github.io/rea/) · [Guides](https://morluto.github.io/rea/guides/) · [DX-Ball showcase](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[Quick start](#quick-start) · [Current status](#current-status) · [Investigation model](#the-investigation-model) · [Tool catalog](#tool-catalog-for-investigation) · [Roadmap](#roadmap) · [How it works](#how-it-works)
|
||||
|
||||
<code>npx rea-agents setup</code>
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[الموقع (بالإنجليزية)](https://morluto.github.io/rea/) · [أدلة الاستخدام](https://morluto.github.io/rea/guides/) · [دراسة حالة DX-Ball](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[البدء السريع](#البدء-السريع) · [حالة الدعم](#حالة-الدعم) · [من الملف التنفيذي إلى السلوك](#من-الملف-التنفيذي-إلى-السلوك) · [منصة أدوات التحقيق](#منصة-أدوات-التحقيق) · [خطة العمل](#خطة-العمل) · [كيف يعمل؟](#كيف-يعمل)
|
||||
|
||||
<table aria-label="REA community">
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[ウェブサイト(英語)](https://morluto.github.io/rea/) · [ガイド](https://morluto.github.io/rea/guides/) · [DX-Ball の事例](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[クイックスタート](#クイックスタート) · [現在の対応状況](#現在の対応状況) · [バイナリから動作へ](#バイナリから動作へ) · [調査ツールカタログ](#調査ツールカタログ) · [ロードマップ](#ロードマップ) · [仕組み](#仕組み)
|
||||
|
||||
<table aria-label="REA community">
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[웹사이트(영문)](https://morluto.github.io/rea/) · [가이드](https://morluto.github.io/rea/guides/) · [DX-Ball 사례](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[빠른 시작](#빠른-시작) · [현재 지원 범위](#현재-지원-범위) · [바이너리에서 동작까지](#바이너리에서-동작까지) · [조사 도구 카탈로그](#조사-도구-카탈로그) · [로드맵](#로드맵) · [작동 방식](#작동-방식)
|
||||
|
||||
<table aria-label="REA community">
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
|
||||
<a href="https://trendshift.io/repositories/82054?utm_source=repository-badge&utm_medium=badge&utm_campaign=badge-repository-82054" target="_blank" rel="noopener noreferrer"><img src="https://trendshift.io/api/badge/repositories/82054" alt="morluto%2Frea | Trendshift" width="250" height="55"/></a>
|
||||
|
||||
**[网站(英文)](https://morluto.github.io/rea/) · [使用指南](https://morluto.github.io/rea/guides/) · [DX-Ball 案例](https://morluto.github.io/rea/showcase/dx-ball/)**
|
||||
|
||||
[快速开始](#快速开始) · [当前状态](#当前状态) · [从二进制到行为](#从二进制到行为) · [调查工具目录](#调查工具目录) · [路线图](#路线图) · [工作原理](#工作原理)
|
||||
|
||||
<table aria-label="REA community">
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
struct ChildBatch<Element> {
|
||||
let values: [Element]
|
||||
let complete: Bool
|
||||
}
|
||||
|
||||
func captureChildValues<Element>(requestedCount: Int, copy: () -> [Element]?) -> ChildBatch<Element> {
|
||||
guard requestedCount > 0 else { return ChildBatch(values: [], complete: true) }
|
||||
let returned = copy() ?? []
|
||||
let values = Array(returned.prefix(requestedCount))
|
||||
return ChildBatch(values: values, complete: values.count == requestedCount)
|
||||
}
|
||||
@@ -26,11 +26,12 @@ func childCount(_ element: AXUIElement) -> Int {
|
||||
var count = 0
|
||||
return AXUIElementGetAttributeValueCount(element, kAXChildrenAttribute as CFString, &count) == .success ? count : 0
|
||||
}
|
||||
func children(_ element: AXUIElement, limit: Int) -> [AXUIElement] {
|
||||
var value: CFArray?
|
||||
let count = min(limit, childCount(element))
|
||||
if count == 0 { return [] }
|
||||
return AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, 0, count, &value) == .success ? value as? [AXUIElement] ?? [] : []
|
||||
func children(_ element: AXUIElement, count: Int) -> ChildBatch<AXUIElement> {
|
||||
captureChildValues(requestedCount: count) {
|
||||
var value: CFArray?
|
||||
guard AXUIElementCopyAttributeValues(element, kAXChildrenAttribute as CFString, 0, count, &value) == .success else { return nil }
|
||||
return value as? [AXUIElement]
|
||||
}
|
||||
}
|
||||
func text(_ element: AXUIElement, _ key: String) -> Any { (attribute(element, key) as? String) ?? NSNull() as Any }
|
||||
func windowBounds(_ element: AXUIElement) -> CGRect? {
|
||||
@@ -115,9 +116,12 @@ func observe(_ request: Request) async throws -> [String: Any] {
|
||||
if path.count >= 32 { if totalChildren > 0 { truncated = true }; continue }
|
||||
let available = max(0, request.max_nodes - nodes.count - pending.count)
|
||||
let count = min(available, totalChildren)
|
||||
let items = children(element, limit: count)
|
||||
let batch = children(element, count: count)
|
||||
if !batch.complete { truncated = true }
|
||||
if totalChildren > count { truncated = true }
|
||||
for index in (0..<count).reversed() { pending.append((items[index], path + [index])) }
|
||||
for (index, item) in batch.values.enumerated().reversed() {
|
||||
pending.append((item, path + [index]))
|
||||
}
|
||||
}
|
||||
}
|
||||
var screenshot: Any = NSNull()
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -39,9 +39,20 @@ explicit limitation; architecture inventory locations remain available.
|
||||
vtable descriptors. Records include exact virtual addresses/file offsets and
|
||||
artifact evidence. Vtable indexes are metadata word offsets; witness indexes
|
||||
start after the conformance header. Names unavailable in metadata remain null.
|
||||
Chained fixups, external bindings, authenticated pointers, categories,
|
||||
properties, generic/resilient/async/coroutine tables and inherited overrides
|
||||
have explicit unsupported or partial coverage. Other providers retain the
|
||||
Pointers are decoded through the image's fixups:
|
||||
- `LC_DYLD_CHAINED_FIXUPS` rebases and binds, including the authenticated
|
||||
arm64e formats;
|
||||
- legacy `LC_DYLD_INFO` bind opcodes.
|
||||
|
||||
So an external superclass resolves from its `_OBJC_CLASS_$_` bind (for
|
||||
example `NSObject`). The `pointer_fixups` coverage facet names the mechanism.
|
||||
Class properties, with parsed attributes, and `__objc_catlist` categories are
|
||||
also decoded. Categories record the extended class, local or external, plus
|
||||
their methods, protocols and properties; category methods appear as
|
||||
implementations with a `category`. Swift field-offset globals that are only
|
||||
initialized at runtime stay unresolved. Generic, resilient, async and
|
||||
coroutine tables, and inherited overrides, have explicit unsupported or
|
||||
partial coverage. Other providers retain the
|
||||
existing symbol-based inventory with its narrower coverage.
|
||||
|
||||
## Native instruction, call and type primitives
|
||||
@@ -165,8 +176,10 @@ GUI authority, and REA never falls back automatically to Hopper.
|
||||
instruction/call facts, value dependencies and process/project cleanup.
|
||||
- `npm run verify:ghidra:aarch64-jump-table`: optimized ELF and byte/halfword
|
||||
relative tables, plus ARM64 Mach-O on an ARM64 macOS host.
|
||||
- `npm run verify:apple-dispatch`: source-built Objective-C protocols/classes and
|
||||
Swift conformances/vtables, repeated after stripping local symbols.
|
||||
- `npm run verify:apple-dispatch`: source-built Objective-C protocols, classes,
|
||||
properties and categories, and Swift conformances and vtables. Each is linked
|
||||
with legacy `LC_DYLD_INFO` and with chained fixups (and as arm64e on Apple
|
||||
silicon), then repeated after stripping local symbols.
|
||||
- `npm run verify:native-ui`: one source-owned fixture window, successful
|
||||
selected-window capture and actions, changed-target rejection, and cleanup.
|
||||
Missing OS permissions fail this lane.
|
||||
|
||||
@@ -669,9 +669,9 @@
|
||||
"mcp_prompts": 6
|
||||
},
|
||||
"digests": {
|
||||
"tools_sha256": "967c3d60f973aeb86a90a5772a217bef90cc5e48d2eea9621d3a2cc2a68834eb",
|
||||
"tools_sha256": "68769748b624a288a5f4c57374de2d6fa5c2b5f5586fbbdef109795c907b7b7b",
|
||||
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
|
||||
"combined_sha256": "e893b9df15ed6eec249c8f9adb583dc4870c240f9126f8e42680a014cd6fba90",
|
||||
"combined_sha256": "44b30e8892c84cf27766d5739228268b8a85f885c63002157bc134ed8d636bb8",
|
||||
"providers_sha256": "a7d885531ee25d42c51385444faec544d34cd7eba6250a7dc75c4faedd4454a3"
|
||||
}
|
||||
}
|
||||
|
||||
+49
-14
@@ -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": [
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
"vitepress": {
|
||||
"entry": ["docs/.vitepress/config.ts"]
|
||||
},
|
||||
"ignoreBinaries": ["mkfifo"],
|
||||
"ignoreDependencies": ["electron", "esbuild", "webpack"],
|
||||
"ignoreExportsUsedInFile": true,
|
||||
"rules": {
|
||||
|
||||
+7
-5
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -22,7 +22,7 @@ delete verifierEnvironment.REA_ILSPY_CMD_PATH;
|
||||
|
||||
const { stdout } = await exec(
|
||||
process.execPath,
|
||||
[join(root, "scripts/verify-managed-conformance.mjs")],
|
||||
[join(root, "scripts/verify/managed/conformance.mjs")],
|
||||
{
|
||||
cwd: root,
|
||||
env: verifierEnvironment,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { McpServer } from "@modelcontextprotocol/server";
|
||||
|
||||
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managedWorkflowToolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "../dist/contracts/managed/managedWorkflowToolContracts.js";
|
||||
import { auxiliaryAnalysisProviderDeclarations } from "../dist/composition/auxiliaryAnalysisProviders.js";
|
||||
import { toolRegistrationOptions } from "../dist/server/toolRegistrationOptions.js";
|
||||
import { ensureGeneratedFile } from "./lib/generated-file.mjs";
|
||||
|
||||
@@ -20,7 +20,8 @@ import time
|
||||
# Keep this digest aligned with LinuxHopper.ts and rerun real-Hopper verification
|
||||
# before accepting another vendor build.
|
||||
SUPPORTED_HOPPER_SHA256 = {
|
||||
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded"
|
||||
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded",
|
||||
"1339f9e58377442b0c6fcb0dfc3cec20d593cc557408521fad9a00dbc6b8da13",
|
||||
}
|
||||
EXPECTED_SCREEN = (1280, 1024)
|
||||
EXPECTED_DIALOG = (189, 370, 901, 284)
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -15,6 +15,23 @@ const root = await mkdtemp(join(tmpdir(), "rea-ui-fixture-"));
|
||||
let child;
|
||||
let fixturePid;
|
||||
try {
|
||||
const childRetrievalTest = join(root, "child-retrieval-test");
|
||||
await promisify(execFile)("/usr/bin/xcrun", [
|
||||
"swiftc",
|
||||
fileURLToPath(
|
||||
new URL("../bridge/native/NativeUIChildren.swift", import.meta.url),
|
||||
),
|
||||
fileURLToPath(
|
||||
new URL(
|
||||
"../tests/conformance/native/native-ui-children/main.swift",
|
||||
import.meta.url,
|
||||
),
|
||||
),
|
||||
"-o",
|
||||
childRetrievalTest,
|
||||
]);
|
||||
await promisify(execFile)(childRetrievalTest, []);
|
||||
|
||||
const contents = join(root, "Fixture.app", "Contents");
|
||||
await mkdir(join(contents, "MacOS"), { recursive: true });
|
||||
await writeFile(
|
||||
@@ -82,7 +99,6 @@ try {
|
||||
"capture_native_ui_scenario",
|
||||
{
|
||||
...scope,
|
||||
restore: "leave-as-is",
|
||||
screenshot: false,
|
||||
steps: [
|
||||
{ kind: "click", path: button.path },
|
||||
@@ -111,8 +127,6 @@ try {
|
||||
String(scope.pid),
|
||||
"--window-id",
|
||||
String(scope.window_id),
|
||||
"--restore",
|
||||
"leave-as-is",
|
||||
"--steps",
|
||||
JSON.stringify([
|
||||
{ kind: "click", path: button.path },
|
||||
|
||||
@@ -2,7 +2,7 @@ import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { createEvidence } from "../dist/domain/evidence.js";
|
||||
import { buildManagedPeFixture } from "./lib/managed-pe-fixture.mjs";
|
||||
import { buildManagedPeFixture } from "./fixtures/managed/pe.mjs";
|
||||
import { functionDossier, json, run } from "./lib/verify-package-core.mjs";
|
||||
|
||||
const buildManagedFixtures = async (workspace) => {
|
||||
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
import { createEvidence } from "../../dist/domain/evidence.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
|
||||
const COMPLETION_PROVIDER = {
|
||||
id: "rea-managed-conformance",
|
||||
@@ -3,21 +3,21 @@ import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { inspectManagedArtifactBytes } from "../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { inspectManagedNativeBoundariesBytes } from "../dist/dotnet/ManagedNativeBoundaryInspector.js";
|
||||
import { compareManagedMemberPaths } from "../dist/application/ManagedMemberComparisonService.js";
|
||||
import { verifyManagedNativeBoundariesEvidence } from "../dist/application/ManagedNativeVerificationService.js";
|
||||
import { importManagedReconstructionEvidence } from "../dist/application/ManagedReconstructionService.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../dist/application/ManagedApplicationGraphService.js";
|
||||
import { traceApplicationFeatureEvidence } from "../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../dist/application/InvestigationProviders.js";
|
||||
import { createEvidence } from "../dist/domain/evidence.js";
|
||||
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { inspectManagedNativeBoundariesBytes } from "../../../dist/dotnet/ManagedNativeBoundaryInspector.js";
|
||||
import { compareManagedMemberPaths } from "../../../dist/application/managed/ManagedMemberComparisonService.js";
|
||||
import { verifyManagedNativeBoundariesEvidence } from "../../../dist/application/managed/ManagedNativeVerificationService.js";
|
||||
import { importManagedReconstructionEvidence } from "../../../dist/application/managed/ManagedReconstructionService.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../../../dist/application/managed/ManagedApplicationGraphService.js";
|
||||
import { traceApplicationFeatureEvidence } from "../../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
import {
|
||||
alternateMvid,
|
||||
buildNativePeFixture,
|
||||
} from "./lib/managed-pe-fixture.mjs";
|
||||
import { createManagedConformanceSupport } from "./lib/managed-conformance-support.mjs";
|
||||
} from "../../fixtures/managed/pe.mjs";
|
||||
import { createManagedConformanceSupport } from "./support.mjs";
|
||||
import {
|
||||
comparisonLimits,
|
||||
defaultIlBody,
|
||||
@@ -25,9 +25,12 @@ import {
|
||||
inspectionLimits,
|
||||
memberLimits,
|
||||
nativeBoundaryLimits,
|
||||
} from "./lib/managed-conformance-config.mjs";
|
||||
import { createManagedCompletionReport } from "./lib/managed-completion-report.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
} from "./config.mjs";
|
||||
import { createManagedCompletionReport } from "./completion-report.mjs";
|
||||
import {
|
||||
completeVerifierRun,
|
||||
createVerifierRun,
|
||||
} from "../../lib/verifier-run.mjs";
|
||||
|
||||
const verifierRun = createVerifierRun();
|
||||
const workspace = await mkdtemp(join(tmpdir(), "rea-managed-conformance-"));
|
||||
@@ -1,18 +1,18 @@
|
||||
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
|
||||
import { inspectManagedArtifactBytes } from "../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../../dist/application/ManagedApplicationGraphService.js";
|
||||
import { traceApplicationFeatureEvidence } from "../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
|
||||
import { createEvidence } from "../../dist/domain/evidence.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../dist/application/InvestigationProviders.js";
|
||||
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
|
||||
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../../../dist/application/managed/ManagedApplicationGraphService.js";
|
||||
import { traceApplicationFeatureEvidence } from "../../../dist/application/javascript/JavaScriptApplicationWorkflowService.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
|
||||
import {
|
||||
ensure,
|
||||
methodTokenRow,
|
||||
parseManagedAppManifest,
|
||||
} from "./managed-conformance-manifest-parser.mjs";
|
||||
} from "./manifest-parser.mjs";
|
||||
|
||||
export const createManagedManifestVerifier = (context) => ({
|
||||
verifyManagedAppManifest: (rawManifest, manifestPath) =>
|
||||
@@ -5,12 +5,12 @@ import { basename, isAbsolute, join, resolve } from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
import { promisify } from "node:util";
|
||||
|
||||
import { inspectManagedArtifactBytes } from "../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { importManagedReconstructionEvidence } from "../../dist/application/ManagedReconstructionService.js";
|
||||
import { createEvidence } from "../../dist/domain/evidence.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../dist/application/InvestigationProviders.js";
|
||||
import { createManagedManifestVerifier } from "./managed-conformance-manifest.mjs";
|
||||
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
import { inspectManagedMembersBytes } from "../../../dist/dotnet/ManagedMemberInspector.js";
|
||||
import { importManagedReconstructionEvidence } from "../../../dist/application/managed/ManagedReconstructionService.js";
|
||||
import { createEvidence } from "../../../dist/domain/evidence.js";
|
||||
import { MANAGED_STATIC_PROVIDER } from "../../../dist/application/InvestigationProviders.js";
|
||||
import { createManagedManifestVerifier } from "./manifest.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -2,9 +2,9 @@ import { createHash } from "node:crypto";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
|
||||
import { buildManagedPeFixture } from "./managed-pe-fixture.mjs";
|
||||
import { createManagedConformanceOracleSupport } from "./managed-conformance-oracles.mjs";
|
||||
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
|
||||
import { buildManagedPeFixture } from "../../fixtures/managed/pe.mjs";
|
||||
import { createManagedConformanceOracleSupport } from "./oracles.mjs";
|
||||
|
||||
export const createManagedConformanceSupport = (context) => {
|
||||
const fixtureBytes = async (name, bytes) => {
|
||||
@@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
|
||||
metadata:
|
||||
version: "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.
|
||||
|
||||
@@ -32,7 +32,8 @@ export class JadxMcpTransport implements Transport {
|
||||
readonly runId = randomUUID();
|
||||
#spawned: SpawnedOwnedProviderProcess | undefined;
|
||||
#supervisor: ProviderProcessSupervisor | undefined;
|
||||
#buffer = Buffer.alloc(0);
|
||||
#frameBuffer = Buffer.alloc(0);
|
||||
#frameLength = 0;
|
||||
#bytes = 0;
|
||||
#started = false;
|
||||
#closed = false;
|
||||
@@ -61,12 +62,16 @@ export class JadxMcpTransport implements Transport {
|
||||
stdin: "pipe",
|
||||
});
|
||||
this.#spawned = spawned;
|
||||
this.#supervisor = new ProviderProcessSupervisor({
|
||||
...spawned,
|
||||
ownsProcessLifetime: true,
|
||||
cleanup:
|
||||
spawned.cleanup ?? (() => cleanupOwnedProcessGroup(spawned.ownership)),
|
||||
});
|
||||
this.#supervisor = new ProviderProcessSupervisor(
|
||||
{
|
||||
...spawned,
|
||||
ownsProcessLifetime: true,
|
||||
cleanup:
|
||||
spawned.cleanup ??
|
||||
(() => cleanupOwnedProcessGroup(spawned.ownership)),
|
||||
},
|
||||
{ captureStdout: false },
|
||||
);
|
||||
if (
|
||||
spawned.process.stdin === undefined ||
|
||||
spawned.process.stdin === null ||
|
||||
@@ -117,6 +122,8 @@ export class JadxMcpTransport implements Transport {
|
||||
|
||||
async #stop(): Promise<void> {
|
||||
this.#closed = true;
|
||||
this.#frameBuffer = Buffer.alloc(0);
|
||||
this.#frameLength = 0;
|
||||
// A cancellation may race acquisition. Join it before inspecting ownership.
|
||||
await this.#starting?.catch(() => undefined);
|
||||
const process = this.#spawned?.process;
|
||||
@@ -161,22 +168,42 @@ export class JadxMcpTransport implements Transport {
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.#buffer = Buffer.concat([this.#buffer, bytes]);
|
||||
try {
|
||||
let newline: number;
|
||||
while ((newline = this.#buffer.indexOf(10)) !== -1) {
|
||||
if (newline > MAX_FRAME_BYTES)
|
||||
let offset = 0;
|
||||
while (offset < bytes.length) {
|
||||
const newline = bytes.indexOf(10, offset);
|
||||
const end = newline === -1 ? bytes.length : newline;
|
||||
const fragment = bytes.subarray(offset, end);
|
||||
const frameBytes = this.#frameLength + fragment.length;
|
||||
if (frameBytes > MAX_FRAME_BYTES)
|
||||
throw new Error(`JADX MCP frame exceeds ${MAX_FRAME_BYTES} bytes`);
|
||||
const line = this.#buffer.subarray(0, newline).toString("utf8");
|
||||
this.#buffer = this.#buffer.subarray(newline + 1);
|
||||
this.#appendFrame(fragment, frameBytes);
|
||||
if (newline === -1) return;
|
||||
const line = this.#frameBuffer
|
||||
.subarray(0, this.#frameLength)
|
||||
.toString("utf8");
|
||||
this.#frameBuffer = Buffer.alloc(0);
|
||||
this.#frameLength = 0;
|
||||
this.onmessage?.(deserializeMessage(line));
|
||||
offset = newline + 1;
|
||||
}
|
||||
if (this.#buffer.length > MAX_FRAME_BYTES)
|
||||
throw new Error(`JADX MCP frame exceeds ${MAX_FRAME_BYTES} bytes`);
|
||||
} catch (cause) {
|
||||
this.#fail(cause instanceof Error ? cause : new Error(String(cause)));
|
||||
}
|
||||
};
|
||||
#appendFrame(fragment: Buffer, frameBytes: number): void {
|
||||
if (frameBytes > this.#frameBuffer.length) {
|
||||
const capacity = Math.min(
|
||||
MAX_FRAME_BYTES,
|
||||
Math.max(frameBytes, this.#frameBuffer.length * 2, 1),
|
||||
);
|
||||
const grown = Buffer.allocUnsafe(capacity);
|
||||
this.#frameBuffer.copy(grown, 0, 0, this.#frameLength);
|
||||
this.#frameBuffer = grown;
|
||||
}
|
||||
fragment.copy(this.#frameBuffer, this.#frameLength);
|
||||
this.#frameLength = frameBytes;
|
||||
}
|
||||
#fail(error: Error): void {
|
||||
this.#failure ??= error.message;
|
||||
this.onerror?.(error);
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { OfficialToolName } from "../contracts/officialToolContracts.js";
|
||||
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
|
||||
import type { NativeToolName } from "../contracts/nativeToolContracts.js";
|
||||
import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js";
|
||||
import type { ManagedToolName } from "../contracts/managedToolContracts.js";
|
||||
import type { ManagedToolName } from "../contracts/managed/managedToolContracts.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContrac
|
||||
import {
|
||||
isManagedToolName,
|
||||
type ManagedToolName,
|
||||
} from "../contracts/managedToolContracts.js";
|
||||
} from "../contracts/managed/managedToolContracts.js";
|
||||
import { EvidenceIntegrityError } from "../domain/evidenceErrors.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { type AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
installLinuxHopper,
|
||||
linuxHopperLauncherDigestSupported,
|
||||
linuxHopperInstallDisclosure,
|
||||
linuxPackageManagerCommands,
|
||||
linuxSharedLibrariesAvailable,
|
||||
@@ -93,6 +94,20 @@ describe("Linux Hopper host classification", () => {
|
||||
});
|
||||
|
||||
describe("Linux Hopper installation", () => {
|
||||
it.each([
|
||||
[
|
||||
"Debian and Arch",
|
||||
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded",
|
||||
],
|
||||
["RPM", "1339f9e58377442b0c6fcb0dfc3cec20d593cc557408521fad9a00dbc6b8da13"],
|
||||
])("accepts the pinned %s launcher build", (_, digest) => {
|
||||
expect(linuxHopperLauncherDigestSupported(digest)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects an unverified launcher build", () => {
|
||||
expect(linuxHopperLauncherDigestSupported("0".repeat(64))).toBe(false);
|
||||
});
|
||||
|
||||
it("discloses the exact download, integrity evidence, and privileged command", () => {
|
||||
expect(linuxHopperInstallDisclosure("deb", false)).toEqual({
|
||||
downloadUrl:
|
||||
|
||||
@@ -11,8 +11,10 @@ import { execFileOutput } from "../process/ExecFileOutput.js";
|
||||
const execFileAsync = promisify(execFile);
|
||||
const DOWNLOAD_PREFIX = "https://www.hopperapp.com:443/downloader/public/";
|
||||
const MAX_PACKAGE_BYTES = 100_000_000;
|
||||
const SUPPORTED_HOPPER_SHA256 =
|
||||
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded";
|
||||
const SUPPORTED_HOPPER_SHA256 = new Set([
|
||||
"0294ced141cc373468ee22d8343e7dac41980cb05a937994ca81c9f09afe7ded",
|
||||
"1339f9e58377442b0c6fcb0dfc3cec20d593cc557408521fad9a00dbc6b8da13",
|
||||
]);
|
||||
interface LinuxHopperRelease {
|
||||
readonly filename: string;
|
||||
readonly file_length: string;
|
||||
@@ -262,7 +264,7 @@ export const linuxHopperBinarySupported = async (
|
||||
try {
|
||||
const hash = createHash("sha256");
|
||||
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
||||
return hash.digest("hex") === SUPPORTED_HOPPER_SHA256;
|
||||
return linuxHopperLauncherDigestSupported(hash.digest("hex"));
|
||||
} catch (cause: unknown) {
|
||||
// best-effort cleanup: optional build probing; unreadable means unsupported.
|
||||
void cause;
|
||||
@@ -270,6 +272,10 @@ export const linuxHopperBinarySupported = async (
|
||||
}
|
||||
};
|
||||
|
||||
/** Match a launcher extracted from one of the pinned official Linux packages. */
|
||||
export const linuxHopperLauncherDigestSupported = (digest: string): boolean =>
|
||||
SUPPORTED_HOPPER_SHA256.has(digest);
|
||||
|
||||
/** Interpret ldd output conservatively so a present but broken launcher is unhealthy. */
|
||||
export const linuxSharedLibrariesAvailable = (output: string): boolean =>
|
||||
!/(?:=>\s+not found|error while loading shared libraries)/iu.test(output);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -52,6 +52,11 @@ export class FakeSetupHost implements SetupHost {
|
||||
linuxDistribution = (): Promise<LinuxDistribution | undefined> =>
|
||||
Promise.resolve(this.distribution);
|
||||
hopperPath = (): Promise<string | undefined> => Promise.resolve(this.hopper);
|
||||
initialSetupState = async (scope?: DoctorScope) => ({
|
||||
...(this.hopper === undefined ? {} : { hopperPath: this.hopper }),
|
||||
providerEnvironment: await this.providerEnvironment(),
|
||||
doctor: await this.doctor(scope),
|
||||
});
|
||||
providerEnvironment = (): Promise<SetupProviderEnvironment> =>
|
||||
Promise.resolve({
|
||||
...(this.hopper === undefined
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { runSetup } from "./Setup.js";
|
||||
import { FakeSetupHost, options } from "./Setup.fixture.js";
|
||||
|
||||
it("uses the shared initial host snapshot for setup planning", async () => {
|
||||
const host = new FakeSetupHost();
|
||||
host.hopperPath = () => {
|
||||
throw new Error("setup snapshot should provide the Hopper path");
|
||||
};
|
||||
host.providerEnvironment = () => {
|
||||
throw new Error("setup snapshot should provide provider settings");
|
||||
};
|
||||
host.initialSetupState = async (scope) => ({
|
||||
hopperPath: "/snapshot/Hopper",
|
||||
providerEnvironment: { HOPPER_LAUNCHER_PATH: "/snapshot/Hopper" },
|
||||
doctor: await host.doctor(scope),
|
||||
});
|
||||
host.clients = [{ name: "codex", configPath: "/codex.json" }];
|
||||
|
||||
const result = await runSetup(
|
||||
{ ...options(false), dryRun: true, clientIds: ["codex"] },
|
||||
host,
|
||||
);
|
||||
|
||||
expect(result.status).toBe("planned");
|
||||
expect(result.plannedActions.map(({ id }) => id)).toContain(
|
||||
"configure_client:codex",
|
||||
);
|
||||
expect(host.doctorCalls).toBe(1);
|
||||
});
|
||||
|
||||
it("refreshes doctor after approved setup changes", async () => {
|
||||
const host = new FakeSetupHost();
|
||||
host.doctorHealthy = false;
|
||||
host.skill = "unchanged";
|
||||
host.clients = [{ name: "codex", configPath: "/codex.json" }];
|
||||
const configureClient = host.configureClient;
|
||||
host.configureClient = (client, providerEnvironment, command) => {
|
||||
host.doctorHealthy = true;
|
||||
return configureClient(client, providerEnvironment, command);
|
||||
};
|
||||
|
||||
const result = await runSetup(
|
||||
{ ...options(true), clientIds: ["codex"], installSkill: false },
|
||||
host,
|
||||
);
|
||||
|
||||
expect(result.doctor.healthy).toBe(true);
|
||||
expect(host.doctorCalls).toBe(2);
|
||||
});
|
||||
@@ -42,8 +42,15 @@ export const runSetup = async (
|
||||
);
|
||||
const unsupported = await hostRemediation(host, false);
|
||||
if (unsupported !== undefined) return fail(unsupported);
|
||||
let hopperPath = await host.hopperPath();
|
||||
let providerEnvironment = await initialProviderEnvironment(host, hopperPath);
|
||||
const initialState = await host.initialSetupState?.(options.readinessScope);
|
||||
let hopperPath =
|
||||
initialState === undefined
|
||||
? await host.hopperPath()
|
||||
: initialState.hopperPath;
|
||||
let providerEnvironment =
|
||||
initialState === undefined
|
||||
? await initialProviderEnvironment(host, hopperPath)
|
||||
: initialState.providerEnvironment;
|
||||
const discovery = await discoverSetupState({
|
||||
host,
|
||||
providerEnvironment,
|
||||
@@ -51,6 +58,9 @@ export const runSetup = async (
|
||||
proposeHopper:
|
||||
options.proposeHopper ?? (confirm !== undefined && !options.structured),
|
||||
doctorScope: options.readinessScope,
|
||||
...(initialState === undefined
|
||||
? {}
|
||||
: { initialDoctor: initialState.doctor }),
|
||||
});
|
||||
clientStates = discovery.clientStates;
|
||||
const clientSelectionAllowed =
|
||||
|
||||
@@ -22,7 +22,12 @@ import {
|
||||
} from "./SetupClientConfiguration.js";
|
||||
import { setupInstallFailure } from "./SetupInstallFailure.js";
|
||||
import { providerRegistrationEnvironment } from "./SetupRegistrationEnvironment.js";
|
||||
import type { SetupHost, SetupProviderEnvironment } from "./SetupTypes.js";
|
||||
import type {
|
||||
SetupHost,
|
||||
SetupInitialState,
|
||||
SetupProviderEnvironment,
|
||||
} from "./SetupTypes.js";
|
||||
import type { DoctorScope } from "./Doctor.js";
|
||||
|
||||
/** Resolve the executable and arguments used in a managed MCP registration. */
|
||||
export const setupRegistrationCommand = (
|
||||
@@ -91,6 +96,25 @@ export const systemSetupHost = (
|
||||
macosVersion: () => doctorHost.macosVersion(),
|
||||
linuxDistribution: readLinuxDistribution,
|
||||
hopperPath: async () => (await runDoctor(undefined, doctorHost)).hopperPath,
|
||||
initialSetupState: async (
|
||||
scope?: DoctorScope,
|
||||
): Promise<SetupInitialState> => {
|
||||
const diagnosis = await runDoctor(undefined, doctorHost, scope);
|
||||
return {
|
||||
...(diagnosis.hopperPath === undefined
|
||||
? {}
|
||||
: { hopperPath: diagnosis.hopperPath }),
|
||||
providerEnvironment: {
|
||||
...providerRegistrationEnvironment(
|
||||
diagnosis.providerInspections ?? [],
|
||||
),
|
||||
...(diagnosis.hopperPath === undefined
|
||||
? {}
|
||||
: { HOPPER_LAUNCHER_PATH: diagnosis.hopperPath }),
|
||||
},
|
||||
doctor: diagnosis,
|
||||
};
|
||||
},
|
||||
providerEnvironment: async () => {
|
||||
const diagnosis = await runDoctor(undefined, doctorHost);
|
||||
return {
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
} from "./SetupTypes.js";
|
||||
import type { SetupClient } from "./SupportedClients.js";
|
||||
import type { DoctorScope } from "./Doctor.js";
|
||||
import type { DoctorReport } from "./Doctor.js";
|
||||
|
||||
/** Read-only setup facts shared by client selection and preflight. */
|
||||
export interface SetupDiscovery {
|
||||
@@ -36,8 +37,10 @@ export const discoverSetupState = async (input: {
|
||||
readonly forceHopperInstall: boolean;
|
||||
readonly proposeHopper: boolean;
|
||||
readonly doctorScope: DoctorScope | undefined;
|
||||
readonly initialDoctor?: DoctorReport;
|
||||
}): Promise<SetupDiscovery> => {
|
||||
const initialDoctor = await input.host.doctor(input.doctorScope);
|
||||
const initialDoctor =
|
||||
input.initialDoctor ?? (await input.host.doctor(input.doctorScope));
|
||||
const linuxHopperRepairNeeded = initialDoctor.checks.some(
|
||||
({ name, ok, detail }) =>
|
||||
!ok &&
|
||||
|
||||
@@ -9,6 +9,13 @@ import type {
|
||||
/** Exact non-secret provider variables propagated into managed registrations. */
|
||||
export type SetupProviderEnvironment = Readonly<Record<string, string>>;
|
||||
|
||||
/** Read-only host facts shared by the initial setup plan. */
|
||||
export interface SetupInitialState {
|
||||
readonly hopperPath?: string;
|
||||
readonly providerEnvironment: SetupProviderEnvironment;
|
||||
readonly doctor: DoctorReport;
|
||||
}
|
||||
|
||||
/** Result of one backup/write/readback transaction. */
|
||||
export type ClientConfigurationResult =
|
||||
| {
|
||||
@@ -38,6 +45,7 @@ export interface SetupHost {
|
||||
macosVersion(): Promise<string | undefined>;
|
||||
linuxDistribution(): Promise<LinuxDistribution | undefined>;
|
||||
hopperPath(): Promise<string | undefined>;
|
||||
initialSetupState?(scope?: DoctorScope): Promise<SetupInitialState>;
|
||||
installHopper(replaceExisting: boolean): Promise<SetupHopperInstallResult>;
|
||||
providerEnvironment?(): Promise<SetupProviderEnvironment>;
|
||||
detectedClients(): Promise<readonly SetupClient[]>;
|
||||
|
||||
+8
-8
@@ -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("/")
|
||||
|
||||
+7
-7
@@ -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;
|
||||
|
||||
+10
-10
@@ -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 = (
|
||||
+7
-7
@@ -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;
|
||||
|
||||
+4
-4
@@ -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 = () =>
|
||||
+7
-7
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -143,14 +143,29 @@ const asarFailure = (
|
||||
path: string,
|
||||
operation: string,
|
||||
cause: unknown,
|
||||
): ArtifactReaderFailure =>
|
||||
cause instanceof ArtifactReaderFailure
|
||||
? cause
|
||||
: new ArtifactReaderFailure(
|
||||
"format",
|
||||
`Malformed or unreadable ASAR during ${operation}: ${path}`,
|
||||
{ cause },
|
||||
);
|
||||
): ArtifactReaderFailure => {
|
||||
if (cause instanceof ArtifactReaderFailure) return cause;
|
||||
if (isFilesystemFailure(cause))
|
||||
return new ArtifactReaderFailure(
|
||||
"io",
|
||||
`Could not ${operation} ASAR at ${path}: ${cause.message}`,
|
||||
{ cause },
|
||||
);
|
||||
return new ArtifactReaderFailure(
|
||||
"format",
|
||||
`Malformed ASAR during ${operation}: ${path}`,
|
||||
{ cause },
|
||||
);
|
||||
};
|
||||
|
||||
const isFilesystemFailure = (
|
||||
cause: unknown,
|
||||
): cause is NodeJS.ErrnoException & Error =>
|
||||
cause instanceof Error &&
|
||||
"errno" in cause &&
|
||||
typeof cause.errno === "number" &&
|
||||
"syscall" in cause &&
|
||||
typeof cause.syscall === "string";
|
||||
|
||||
const isMissingFile = (cause: unknown): boolean =>
|
||||
typeof cause === "object" &&
|
||||
|
||||
@@ -85,7 +85,7 @@ export class DirectoryArtifactReader implements ArtifactReader {
|
||||
);
|
||||
const handle = await open(
|
||||
entry.adapterKey,
|
||||
constants.O_RDONLY | constants.O_NOFOLLOW,
|
||||
constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK,
|
||||
);
|
||||
try {
|
||||
const observed = await handle.stat();
|
||||
|
||||
@@ -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"]));
|
||||
|
||||
+6
-6
@@ -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)
|
||||
+42
-33
@@ -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;
|
||||
@@ -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);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { z } from "incur";
|
||||
|
||||
import { compareManagedMemberPaths } from "../application/ManagedMemberComparisonService.js";
|
||||
import { verifyManagedNativeBoundariesEvidence } from "../application/ManagedNativeVerificationService.js";
|
||||
import { importManagedReconstructionEvidence } from "../application/ManagedReconstructionService.js";
|
||||
import { compareManagedMemberPaths } from "../application/managed/ManagedMemberComparisonService.js";
|
||||
import { verifyManagedNativeBoundariesEvidence } from "../application/managed/ManagedNativeVerificationService.js";
|
||||
import { importManagedReconstructionEvidence } from "../application/managed/ManagedReconstructionService.js";
|
||||
import { runProviderAnalysis } from "../application/DirectAnalysis.js";
|
||||
import { parseCliJsonInput } from "../cliJsonInput.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { z } from "incur";
|
||||
|
||||
import { projectManagedApplicationGraphEvidence } from "../application/ManagedApplicationGraphService.js";
|
||||
import { projectManagedApplicationGraphEvidence } from "../application/managed/ManagedApplicationGraphService.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { parseCliJsonInput } from "../cliJsonInput.js";
|
||||
import { logCliCommand } from "../cliLogging.js";
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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";
|
||||
|
||||
+4
-4
@@ -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({
|
||||
+3
-3
@@ -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",
|
||||
+6
-6
@@ -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",
|
||||
},
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
@@ -80,7 +80,7 @@ export const SESSION_TOOL_CONTRACTS = [
|
||||
),
|
||||
session(
|
||||
"capture_process_scenario",
|
||||
"Run one caller-selected command under a PTY and return process capture Evidence with residual unknowns. A command name resolves through the inherited PATH; the working directory defaults to the caller's current directory; host environment variables are inherited with scenario overrides. Filesystem snapshots are opt-in through filesystem_observation_paths. The target runs with the current user's permissions; this is not a security sandbox.",
|
||||
"Run one caller-selected command under a PTY and return process capture Evidence with residual unknowns. A command name resolves through the inherited PATH; the working directory defaults to the caller's current directory; host environment variables are inherited with scenario overrides. Environment keys cannot contain '=' or NUL, and REA_PROCESS_RUN_ID is reserved for process ownership. Filesystem snapshots are opt-in through filesystem_observation_paths. The target runs with the current user's permissions; this is not a security sandbox.",
|
||||
processScenarioSchema,
|
||||
),
|
||||
session(
|
||||
|
||||
@@ -5,10 +5,10 @@ import { OFFICIAL_TOOL_CONTRACTS } from "./officialToolContracts.js";
|
||||
import { SESSION_TOOL_CONTRACTS } from "./sessionToolContracts.js";
|
||||
import { TOOL_CONTRACTS } from "./toolContracts.js";
|
||||
import { ARTIFACT_TOOL_CONTRACTS } from "./artifactToolContracts.js";
|
||||
import { MANAGED_TOOL_CONTRACTS } from "./managedToolContracts.js";
|
||||
import { MANAGED_TOOL_CONTRACTS } from "./managed/managedToolContracts.js";
|
||||
import { FIRMWARE_TOOL_CONTRACTS } from "./firmware/firmwareToolContracts.js";
|
||||
import { ANDROID_TOOL_CONTRACTS } from "./android/androidToolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managedWorkflowToolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managed/managedWorkflowToolContracts.js";
|
||||
import { NATIVE_TOOL_CONTRACTS } from "./nativeToolContracts.js";
|
||||
import { BROWSER_PROVIDER_TOOL_CONTRACTS } from "./browserProviderToolContracts.js";
|
||||
import { ELECTRON_TOOL_CONTRACTS } from "./javascript/electronToolContracts.js";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { NATIVE_TOOL_CONTRACTS } from "./nativeToolContracts.js";
|
||||
import { ARTIFACT_TOOL_CONTRACTS } from "./artifactToolContracts.js";
|
||||
import { MANAGED_TOOL_CONTRACTS } from "./managedToolContracts.js";
|
||||
import { MANAGED_TOOL_CONTRACTS } from "./managed/managedToolContracts.js";
|
||||
import { FIRMWARE_TOOL_CONTRACTS } from "./firmware/firmwareToolContracts.js";
|
||||
import { ANDROID_TOOL_CONTRACTS } from "./android/androidToolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managedWorkflowToolContracts.js";
|
||||
import { MANAGED_WORKFLOW_TOOL_CONTRACTS } from "./managed/managedWorkflowToolContracts.js";
|
||||
import { BROWSER_PROVIDER_TOOL_CONTRACTS } from "./browserProviderToolContracts.js";
|
||||
import { ELECTRON_TOOL_CONTRACTS } from "./javascript/electronToolContracts.js";
|
||||
import { JAVASCRIPT_RUNTIME_OBSERVATION_TOOL_CONTRACTS } from "./javascript/javascriptRuntimeObservationToolContracts.js";
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user