# 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. ```sh rea project-apple-application-graph '{"inventory_evidence":[]}' ``` ```json { "name": "project_apple_application_graph", "arguments": { "inventory_evidence": [""] } } ``` The result includes every component in each inventory category and every JavaScript-to-native bridge candidate pair. It has no caller-selected component budget, prefix truncation, or omitted-count fields. Duplicate inventory pages are merged by artifact identity and occurrence path; their Evidence IDs remain attached as source Evidence. 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/.app` | | `.app` directory | `.`, the inventoried bundle itself | | Directory, ZIP, or DMG containing apps | Each outermost `.app` followed by `Contents/` | ZIP archives made with `ditto -c -k --keepParent` report `.app`. A DMG reports `.dmg//.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//` | | `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//` 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//` 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`, `shared-cache`, `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, or in the supplied shared cache, resolves, but an earlier candidate that REA cannot decide (for example `/opt/vendor/lib` or `/usr/lib/swift` without a cache) 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. | | `shared-cache` | An absolute install path, or an `@rpath` candidate outside the root, was found in the dyld shared cache passed as `shared_cache` (CLI `--shared-cache`). dyld loads the cached image, so later in-root candidates are unused. | 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 Mach-O placed at an earlier search path would load first. Whether code-signing library validation would reject it is not evaluated; use `inspect_signature`. - `dyld-environment-present`: `LC_DYLD_ENVIRONMENT` search paths are not modeled. `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`). With `shared_cache`, absolute candidates are looked up in that cache's image list. A process uses the cache only when both of these hold: - the cache serves its CPU family (an arm64e cache serves arm64 and arm64e); - one of its `LC_BUILD_VERSION` or `LC_VERSION_MIN_*` platforms (reported per slice as `platforms`) matches the cache's platform or alternate platform. An iOS cache extracted from an IPSW is therefore not used for a macOS executable. A limitation names each process root the cache was not used for, and why. The result's `shared_cache` names the cache path, UUID, architecture and OS version. Cached images are not traversed. An image listed in the cache but mapped in a missing or UUID-mismatched subcache is `undetermined`. It is listed in `coverage.unverified_shared_cache_images` and makes coverage partial. Without a cache, system paths stay `undetermined`. ## dyld shared cache `inspect_dyld_shared_cache` reads a dyld shared cache file set selected by path. On current macOS this is `/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e`; a cache extracted from an IPSW also works. It runs without an open target. ```sh rea inspect-dyld-shared-cache /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e --image /usr/lib/libSystem.B.dylib --json ``` ```json { "name": "inspect_dyld_shared_cache", "arguments": { "cache_path": "/System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e", "images": ["/usr/lib/libSystem.B.dylib"] } } ``` The result reports: - the magic, architecture, UUID, platform and OS version, any alternate platform (Mac Catalyst), cache type, shared region and maximum slide; - every VM mapping, with the file that holds it; - every subcache by file suffix, with its UUID checked against the main cache. A missing or mismatched subcache makes coverage `partial`, and images mapped there are `unmapped`; - the complete image list with addresses. For each requested install path, the tool reads that image's load commands (install name, dependencies with their re-export, weak, upward and delayed-init flags, and rpaths) through the cache mappings. A path the cache does not contain is `absent`. Older single-file caches are read from their legacy header fields. Nothing is executed, slid or extracted. Slide info, local symbols and the cache's code signature are not parsed. The Evidence subject is the main cache file, identified by its digest.