mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
229 lines
13 KiB
Markdown
229 lines
13 KiB
Markdown
# Apple Application Inventory Projection
|
|
|
|
REA can project one or more authenticated `inventory_artifact` Evidence records
|
|
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.
|
|
|
|
Framework hints match complete path segments such as `React.framework`,
|
|
`ReactNative.framework`, `hermes.framework`, `Flutter.framework`, and
|
|
`UnityFramework.framework`. Substrings in `Preact.framework`,
|
|
`Community.framework`, or `MyApp.framework` do not identify those families.
|
|
A generic `App.framework` also does not identify Flutter by itself. Runtime
|
|
families remain inventory-derived hints, not observed loading. Generic framework
|
|
and dylib paths do not establish Swift or Objective-C; this projection does not
|
|
decode language metadata. A bridge basis is inferred from the native path and
|
|
repeated for each paired script, rather than proving a particular binding.
|
|
|
|
```sh
|
|
rea project-apple-application-graph '{"inventory_evidence":[<inventory_artifact Evidence>]}'
|
|
```
|
|
|
|
```json
|
|
{
|
|
"name": "project_apple_application_graph",
|
|
"arguments": {
|
|
"inventory_evidence": ["<inventory_artifact Evidence>"]
|
|
}
|
|
}
|
|
```
|
|
|
|
The result includes every component in each inventory category. Bridge
|
|
hypotheses are the path-based JavaScript/native pairs within each application
|
|
root. To keep their Cartesian expansion bounded, the projection limits the
|
|
serialized candidate array to 2 MiB. It computes the pair count and
|
|
exact UTF-8 JSON size before creating candidate objects, then retains a
|
|
deterministic prefix when the byte budget is exceeded. The result's
|
|
`bridge_candidate_coverage` reports total, emitted, and omitted pair counts;
|
|
overall coverage becomes `partial` and a limitation explains the omission.
|
|
Duplicate inventory pages are merged by artifact identity and occurrence path;
|
|
their Evidence IDs remain attached as source Evidence.
|
|
|
|
Content nodes identify bytes by SHA-256 and report only content-derived formats.
|
|
Each occurrence reports its own `artifact_kind`, `artifact_format`, and executable
|
|
permission. Identical bytes can therefore appear as a script, resource, or native
|
|
addon at different paths without overwriting another occurrence's role. Package
|
|
formats such as APK and IPA belong to the occurrence; their ZIP bytes share a
|
|
content format. Graph and directory identities use locale-independent Unicode
|
|
ordering.
|
|
|
|
For IPA inventories, archive components outside every application root remain
|
|
in the component lists. Their JavaScript and native candidates are grouped with
|
|
other unrooted components, preserving the inventory projection's path pairing
|
|
semantics without attributing them to an application bundle.
|
|
|
|
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 arrays remain complete when candidate
|
|
hypotheses are omitted; component arrays still include every component from
|
|
the supplied inventory pages, and overall coverage is `partial` in that case.
|
|
|
|
## 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.
|
|
|
|
## Dylib load resolution
|
|
|
|
`trace_dylib_resolution` answers which file each Mach-O dylib load reaches. It
|
|
runs on the active Mach-O, or on every executable in the active `.app`. Each
|
|
executable in a bundle (the main app, XPC services, app extensions, login items,
|
|
privileged helpers, and helper tools) is a separate process root with its own
|
|
`@executable_path`.
|
|
|
|
```sh
|
|
rea trace-dylib-resolution MyApp.app --json
|
|
rea trace-dylib-resolution MyApp.app --root Contents/MacOS/MyApp --architecture arm64 --json
|
|
```
|
|
|
|
```json
|
|
{
|
|
"name": "trace_dylib_resolution",
|
|
"arguments": { "roots": ["Contents/MacOS/MyApp"], "architecture": "arm64" }
|
|
}
|
|
```
|
|
|
|
Load commands are parsed in TypeScript from Mach-O and FAT headers, including
|
|
the `dylib_use_command` encoding newer linkers emit for flags such as delayed
|
|
initialization. Nothing is executed and no Apple tool is required, so the tool
|
|
also works off macOS.
|
|
|
|
Resolution follows dyld:
|
|
|
|
- `@executable_path` is the directory of the process root's executable. It is
|
|
undetermined when the root is a library.
|
|
- `@loader_path` is the directory of the image that holds the load command, or
|
|
of the image that owns the `LC_RPATH` entry being expanded.
|
|
- `@rpath` tries each `LC_RPATH` of the loading image first, then those of the
|
|
image that loaded it, up to the process root.
|
|
- Dependencies load dependents-first in load-command order, and each image
|
|
loads once per process. A later request whose install name matches an
|
|
already loaded image reuses it.
|
|
- Symlinks such as `Versions/Current` are followed inside the analyzed root.
|
|
An escape through `..` or an absolute link is `escapes-target`.
|
|
|
|
Each edge lists every candidate path tried, with its outcome: `resolved`,
|
|
`absent`, `not-mach-o`, `malformed`, `unsupported` (recognized Mach-O outside
|
|
parser coverage, such as big-endian images), `architecture-missing`,
|
|
`outside-target`, `escapes-target`, or `undetermined`. Slices follow dyld's
|
|
graded architectures, so an `x86_64h` process also loads `x86_64`. The edge's `resolution` is one of:
|
|
|
|
| Status | Meaning |
|
|
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
| `resolved` | An image inside the analyzed root loads, and no earlier candidate was undeterminable. |
|
|
| `conditional` | An image inside the root resolves, but an earlier candidate outside the root (for example `/usr/lib/swift`) would win if it exists on the running system. |
|
|
| `unresolved` | Every candidate is definitively absent or unusable inside the root. |
|
|
| `undetermined` | No candidate resolves inside the root, and some candidate lies outside it or depends on the environment. |
|
|
|
|
A `conditional` image is still traced, because it is the image the analyzed
|
|
root supplies. Every edge below it has `loader_conditional: true`, and a
|
|
request that reuses it by install name is `conditional` too. Findings on those
|
|
edges say that they apply only if the conditional image loads.
|
|
|
|
Absolute install names such as `/usr/lib/libSystem.B.dylib` are
|
|
`outside-target`. On macOS 11 and later most of them live in the dyld shared
|
|
cache rather than on disk, so REA does not check them against the host.
|
|
|
|
`findings` are derived, not observed:
|
|
|
|
- `required-load-unresolved`, `weak-load-unresolved`, and `lazy-load-unresolved`.
|
|
`LC_LAZY_LOAD_DYLIB` dependencies are resolved but not traversed, because dyld
|
|
loads them on first use; an unresolved one is not a launch failure.
|
|
- `earlier-rpath-candidate-absent`: a compatible Mach-O placed at an earlier
|
|
modeled search path could take precedence, subject to unmodeled search inputs
|
|
and code-signing library validation. These findings do not establish which
|
|
image dyld will load; use `inspect_signature` for signing metadata.
|
|
- `dyld-environment-present`: preserves observed `LC_DYLD_ENVIRONMENT` entries.
|
|
Only an executable root supplies process settings; entries on library and
|
|
other non-executable roots do not alter resolution or coverage. Applicable
|
|
executable image-selection overrides are not modeled.
|
|
|
|
Empty dyld settings have different consumers. Modern dyld appends a slash and
|
|
image name to ordinary and fallback directory entries, so an empty entry can
|
|
search `/child.dylib` or `/Foo.framework/Foo`. REA retains uncertainty for those
|
|
unchecked paths; an empty fallback does not preempt an ordinary-search hit.
|
|
Empty versioned-directory lists do not scan a directory, and empty suffixes
|
|
only repeat the original candidate, so they do not add search uncertainty.
|
|
Reported command values are retained in all cases.
|
|
|
|
`verify:macos-bundle` checks the parser against `otool -l` for every traced
|
|
image. It also compares the predicted load order of two process roots with the
|
|
images dyld actually loads (`DYLD_PRINT_LIBRARIES`).
|