A customer’s iOS app got popped. They tell you the version, the date, the symptom, a coordinated batch of fraudulent transactions, an unexplained spike in token-introspection failures, a credential-stuffing rate that doesn’t match their app’s user base. They hand you a .xcarchive from their build server. The original Xcode environment is gone; the CI pipeline that produced it has been rolled forward; the developer who built it just left. You need a defensible report by end of week.
This is incident response on an artifact, not a codebase. The good news: an .xcarchive is structurally richer than an .ipa. It carries debug symbols, the original Info.plist hierarchy, build provenance metadata in Info.plist, and an unencrypted Mach-O. Used right, you can reconstruct enough of the build to answer the four questions that matter:
- What got shipped, exact symbol map, linked libraries, embedded resources
- What was supposed to ship, declared entitlements, provisioning chain, SDK versions
- Where the gap is, anything in (1) that isn’t in (2), or vice versa
- What changed between this version and the last clean one, diffed against a known-good archive
The .xcarchive’s actual structure
An archive is a Bundle directory:
MyApp 2026-05-15, 14.32.xcarchive/
├── Info.plist ← archive metadata: scheme, team ID,
│ distribution method, signing identity
├── Products/
│ └── Applications/
│ └── MyApp.app/ ← the auditable .app bundle
│ ├── Info.plist ← the app's own Info.plist (URL schemes,
│ │ entitlements references, framework load list)
│ ├── MyApp ← the unencrypted Mach-O (no FairPlay)
│ ├── embedded.mobileprovision ← the prov profile USED to build
│ ├── PrivacyInfo.xcprivacy ← declared APIs (iOS 17+)
│ ├── PlugIns/, Frameworks/ ← extensions + dynamic libs
│ └── _CodeSignature/CodeResources
├── dSYMs/ ← debug symbols, symbolicate crash logs,
│ recover function names from optimized code
└── SCMBlueprint/ ← optional: git ref, branch, repo URLs
Three artifacts in there carry more incident-response signal than the rest combined:
SCMBlueprint/tells you the exact git SHA the build came from. If you have repo access, you cangit diff <previous-good-sha>...<this-sha>for the source-level delta.dSYMs/lets you symbolicate every stack frame in the customer’s bug reports + their crash logs. Without dSYMs, an optimized iOS binary’s stack traces are just hex addresses.embedded.mobileprovisioncarries the entitlements that were ACTUALLY signed into the build, not whatentitlements.plistin the project said, but what shipped.
The audit pipeline
The five-stage pattern is the same regardless of the artifact, but each stage takes the archive’s structure into account.
Stage 1, Provenance reconstruction
Before touching the binary, read the archive’s Info.plist:
plutil -p "MyApp.xcarchive/Info.plist"
You’re looking for:
Name, the scheme name (often reveals targets you didn’t know existed)SchemeName, same purpose, set by Xcode-driven buildsCFBundleVersion+CFBundleShortVersionString, what the user saw on their deviceApplicationProperties.IconPaths, accidentally bundled assetsApplicationProperties.SigningIdentity, who signed ("Apple Distribution: Acme Inc."vs"iPhone Developer:")ArchiveVersion, schema version of the archive itself
This is your “who, what, when” for the report. Cross-reference with the customer’s claim of which build hit prod.
Stage 2, Mach-O dissection on the inner .app
Don’t audit the archive bundle, audit Products/Applications/<App>.app/<binary>. The archive itself is metadata; the audit subject is the binary inside.
For a quick triage:
codesign -dvv --entitlements - "MyApp.app"
otool -L "MyApp.app/MyApp" # linked libraries
otool -l "MyApp.app/MyApp" | head -100 # load commands
nm -gU "MyApp.app/MyApp" | wc -l # exported symbols (sanity check)
For real depth: hand it to an audit tool that drives the parsers all at once. SentinelDen Studio treats .xcarchive as a first-class input (drag the archive in from Xcode’s Organizer; it drills to the inner .app automatically) and runs the Mach-O parser, signature inspector, library resolver, privacy-manifest auditor, and symbol browser in one pipeline.
What you’re hunting in this stage:
- Linked libraries you didn’t expect, a
Frameworks/entry the customer’s threat model doesn’t account for (a forgotten analytics SDK, an outdated Crashlytics build, a community Frida-detection library) - Strings the binary contains, backend API endpoints, hardcoded auth tokens, debug flags that should’ve been compiled out (
#if DEBUGblocks that ended up in release because of misconfigured xcconfig) - Symbol table anomalies, symbols from extensions the customer didn’t know they had
Stage 3, Entitlement vs declaration cross-check
This is where most incident-response audits find the real bug.
The embedded.mobileprovision carries the signed entitlements, what iOS will let the app actually do. The app’s Info.plist declares what features it expects. The PrivacyInfo.xcprivacy declares the required-reason APIs it touches. Cross-check:
# What the provisioning profile claims:
security cms -D -i "MyApp.app/embedded.mobileprovision" | plutil -p - \
| grep -A50 Entitlements
# What the app's bundle Info.plist promises:
plutil -p "MyApp.app/Info.plist" | grep -E "URLScheme|Bundle|Background|Network"
# What the privacy manifest declares:
plutil -p "MyApp.app/PrivacyInfo.xcprivacy"
The triple cross-check catches:
- A
keychain-access-groupsentitlement that’s no longer used by any production code (left over from a migration) - A custom URL scheme registered in Info.plist but not actually handled in code (open redirect surface)
- A required-reason API in the privacy manifest with no matching call site in the binary, meaning someone removed a code path without updating the manifest, and the App Store reviewer didn’t notice
Stage 4, Diff against the last clean version
If the customer has the previous .xcarchive from before the incident, diff structurally, not by file bytes, but by audit-meaningful properties:
| Property | v1.4.2 (clean) | v1.4.3 (incident) | Δ |
|---|---|---|---|
| Linked frameworks | 47 | 51 | +4 |
| Strings >12 chars matching URL pattern | 312 | 327 | +15 |
| Embedded provisioning entitlements | 18 | 19 | +1 new: aps-environment=production |
| Privacy manifest required-reason APIs | 8 | 8 | 0 |
| Binary entropy (avg per __TEXT page) | 6.84 | 6.93 | +0.09 |
This is where build-to-build diffing in your audit tool earns its keep. The +4 linked frameworks row is the lead. The new aps-environment=production entitlement is the smoking gun if the customer didn’t think they shipped push.
Stage 5, Report
You’re writing for a customer’s CISO or audit lead, not a security researcher peer. Three sections:
- Confirmed facts, exact versions, signing identity, what shipped vs declared. No interpretation.
- Likely root cause, your hypothesis, with the artifacts that point to it.
- Followups, what they should verify in their build environment, what to retest, what to rebuild from a known-clean state.
Sign the report. PDF, embedded signature, your team’s identity. SARIF + CycloneDX SBOM as machine-readable attachments. Studio emits all three from one audit run; the SARIF makes the findings ingestable by the customer’s own GitHub code-scanning or Snyk dashboard.
What .xcarchive can’t tell you
An archive is the build output, not the build process. It won’t tell you:
- Whether the build environment was compromised (a poisoned
Pods/cache, an SDK pulled from a typosquatted CocoaPods name) - Whether the developer signed under duress
- What CI variables were in scope when the build ran
- Whether
enableTestabilityorswift_enable_module_lookupwas on
You answer those by asking the customer for their CI logs from that build’s run. The archive gives you a starting point; the CI logs give you the chain.
But for incident triage in the first 48 hours, what shipped, what’s anomalous, where the gap is, what changed, the .xcarchive is enough. The audit pipeline runs locally on your machine in minutes, and the report you produce is defensible without the customer’s source.
Drag the archive into Studio. Run the audit. Write the report. Send the customer back to their build pipeline with a specific question. That’s the loop.
Engineering opinion, not advice. This post reflects the author's engineering reasoning at time of publication. It is not professional security, legal, financial, or compliance advice; do not rely on it as a substitute for review by qualified professionals for your specific situation. Posts may become outdated as iOS, Apple frameworks, attacker techniques, and our own SDKs evolve. The canonical source of truth for shipped SDK behavior is /docs and /changelog.

Comments
Nothing yet. Say the first thing.
Sign in to join the conversation.