💡 Pro Tip: Before adding a dependency, ask: "How many other packages does this pull into my project transitively?" If you don't know the answer, your inventory is already incomplete.
Table of Contents
- The invisible face of the dependency chain: transitive packages
- Extracting an inventory in SPM, CocoaPods and Gradle
- SBOM generation and version pinning
- New-dependency acceptance criteria
- Wiring automatic-update and security warnings into CI
- Response plan for a compromised-package scenario
- Quarterly audit template
- FAQ
- How do you extract a dependency inventory in a mobile app?
- What is an SBOM and how is it generated on mobile?
- What should you do if a dependency is compromised?
- In Gradle, which dependency should be added with which configuration?
- What are the version-pinning options in SPM?
- Update (September 2026)
- Conclusion
- Sources
The invisible face of the dependency chain: transitive packages
When you add a dependency, you don't actually pull in just one package — you also pull in every sub-dependency it declares. Android's official Gradle documentation states this explicitly: when you add a library as a dependency, "any transitive dependencies they declare are automatically included as well." In other words, a package that appears as a single line in your build.gradle.kts file can silently pull five or ten more packages into your project behind the scenes.
The key to managing this behavior in Gradle is choosing the right configuration type. A dependency added with api is also transitively "exported" to the consumers of the module that uses it — in the official documentation's words: "When a module includes an api dependency, it's letting Gradle know that the module wants to transitively export that dependency." implementation, on the other hand, stops this leak at the module boundary.
1// build.gradle.kts — configuration type = inventory boundary2dependencies {3 // Exported transitively — consumer modules see it too4 api("com.squareup.retrofit2:retrofit:2.9.0")5 6 // Stays only in this module — keeps the inventory surface small7 implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")8 9 // Added only to the compile classpath, not packaged into the output10 compileOnly("com.google.code.findbugs:jsr305:3.0.2")11 12 // Generates code before compilation, never enters runtime13 ksp("com.google.dagger:dagger-compiler:2.48")14}On the Apple side the model differs but the outcome is similar: Target.Dependency defines that a target can depend on other targets within the same package or on "products" vended by the packages it depends on — in the official wording: "A target may depend on other targets within the same package and on products vended by the package's dependencies." So in SPM the transitive chain is built through the target-product graph; when you add a package, everything defined in its products field becomes part of the surface your project can reach.
The practical consequence: before extracting an inventory, you need to know which configuration type (Gradle) or which product relationship (SPM) actually ends up in the compiled output — otherwise the "10 dependencies" you counted may actually represent a hidden surface of 40 packages.
Extracting an inventory in SPM, CocoaPods and Gradle
All three ecosystems define their dependency list differently, and your inventory-extraction strategy should be shaped accordingly.
In SPM, dependencies are listed in the Package struct's dependencies: [Package.Dependency] field. Apple's official documentation defines this field as "The list of package dependencies." and gives the following example declaration:
1// Package.swift — the starting point of the inventory2let package = Package(3 name: "MyApp",4 dependencies: [5 .package(url: "https://url/of/another/package/named/utility", from: "1.0.0")6 ],7 targets: [8 .target(name: "MyApp", dependencies: ["Utility"])9 ]10)Each .package(...) entry is a separate Package.Dependency object — as the documentation defines it: "A package dependency of a Swift package." The first step of the inventory is to list every entry in this manifest file, either by hand or with a script.
On the CocoaPods side the situation is different: there is no official CocoaPods-specific inventory or SBOM mechanism. A Podfile still keeps a dependency list following the same logic; that's the starting point of the inventory, but CocoaPods itself has no official "extract the inventory" command before third-party tools (general-purpose scanners like Syft or Microsoft SBOM Tool) come into play.
1# Podfile — the raw list of the inventory (CocoaPods has no built-in SBOM tool)2platform :ios, '15.0'3 4target 'MyApp' do5 use_frameworks!6 pod 'Alamofire', '5.9.1'7 pod 'SDWebImage', '5.19.1'8endOn the Android/Gradle side, the inventory is intertwined with configuration types. compileOnly adds a dependency only to the compile classpath and doesn't package it into the output — in the documentation's words: "Gradle adds the dependency to the compile classpath only (that is, it's not added to the build output)." The annotationProcessor, kapt and ksp configurations "supply libraries that process annotations and other symbols in your code before it is compiled" — meaning they run at compile time and never enter runtime at all. If you don't make this distinction when extracting your inventory, you'll overstate the actual number of dependencies that end up in your app.
Ecosystem | Manifest / source | Official inventory mechanism |
|---|---|---|
Gradle (Android) | build.gradle.kts | Configuration types (api/implementation/compileOnly) + Play Console metadata scanning |
Swift Package Manager | Package.swift | Package.Dependency list + Target.Dependency graph |
GitHub (repo-wide) | Dependency graph | SBOM export (SPDX format) |
CocoaPods | Podfile | No official mechanism — a third-party scanner is required |
SBOM generation and version pinning
GitHub offers an SBOM export feature that dumps a repo's dependency inventory into a formal standard. This feature uses an industry-standard format — in the documentation's words, "industry standard SPDX format" — and the export flow is triggered from the Dependency graph section under the repo's Insights tab: "On the top right side of the Dependencies tab, click Export SBOM." Automatic generation via GitHub Actions is also possible; the documentation describes this as "SPDX 2.2 compatible SBOMs."
Knowing what the generated SBOM does and doesn't include is critical: "SBOMs include an inventory of a project's dependencies and associated information such as versions, package identifiers, licenses, transitive paths, and copyright information." So transitive paths are included — but it's one-directional: "SBOMs do not include dependents (other projects that rely on your project)." In other words, an SBOM answers the question "what am I using," not "who is using me."
On the version-pinning side, SPM offers three of the most common pinning methods (the Package.Dependency.Requirement enum behind these methods has been deprecated since SwiftPM 5.6; the current usage is the direct calls package(url:exact:), package(url:from:) and package(url:revision:)):
1// Package.swift — choosing a pinning strategy2dependencies: [3 // Exact pin — only this version, no automatic updates4 .package(url: "https://github.com/vendor/utility", exact: "2.4.0"),5 6 // Controlled automatic updates — major version fixed, minor/patch free7 .package(url: "https://github.com/vendor/networking", from: "3.0.0"),8 9 // Absolute lock at the commit-hash level10 .package(url: "https://github.com/vendor/crypto", revision: "a1b2c3d4e5f6")11]Apple's official definition for the legacy Requirement enum behind these calls reads as follows: exact(_:) — "Returns a requirement for the given exact version."; upToNextMajor(from:) — "A source control requirement bounded to the given version's major version number." and upToNextMinor(from:) applies the same logic at the minor level; revision(_:) — "Returns a requirement for a source control revision such as the hash of a commit." — meaning you can lock a dependency to a specific commit.
On the Gradle/Android side, using dynamic versions is explicitly discouraged: "When specifying dependencies, you shouldn't use dynamic version numbers, such as 'com.android.tools.build:gradle:3.+'. Using this feature can cause unexpected version updates." Instead, BOM (Bill of Materials) support is recommended — "Some libraries are available in a published Bill of Materials (BOM) that groups families of libraries and their versions. You can include a BOM in your version catalog and build files." A BOM is an official mechanism that manages version consistency from a single point.
Strategy | Platform | Update flexibility | Risk profile |
|---|---|---|---|
.exact(_:) | SPM | None — requires manual updates | Lowest surprise risk, highest maintenance burden |
.upToNextMajor(from:) | SPM | Minor/patch automatic | Balanced — the default choice for most teams |
.revision(_:) | SPM | None — locked to a commit | For critical/cryptography dependencies |
BOM + version catalog | Gradle | Centralized, single point | Version consistency, prevents dynamic-version risk |
Dynamic version ( 3.+) | Gradle | Fully automatic | Not officially recommended |
I usually prefer .exact or .revision for dependencies in the networking and cryptography layer; .upToNextMajor is enough for lower-risk packages such as UI component libraries.
New-dependency acceptance criteria
The questions you should ask before accepting a dependency into your project overlap with the ones Android Studio already asks automatically. In the documentation's words, for public SDKs listed in the Google Play SDK Index, Android Studio shows a lint warning in the version catalog file and the Project Structure Dialog in four cases: "The SDKs are marked as outdated by their authors, The SDKs violate Play policies, The SDKs have known security vulnerabilities, The SDKs have been deprecated by their authors." These four criteria can be used directly as an official reference when building your own acceptance checklist.
An additional dimension you should add to your acceptance criteria is which configuration type the dependency will be added with. If a package only does compile-time code generation (ksp/kapt) or is only needed on the compile classpath (compileOnly), adding it with implementation or api unnecessarily enlarges the runtime surface. During acceptance, always ask: "Does this package enter runtime, or does it only run at compile time?"
If you've built a modular architecture (see Modular iOS Architecture and Swift Package Manager), which module a new dependency enters — and which other modules it transitively leaks into — must be part of the acceptance criteria. In projects using dependency injection, placing a third-party SDK behind a protocol instead of injecting it directly as a concrete type squeezes the impact of a future package change down to a single point.
- Version policy: is the package accepted with
.exact/.revisionor with.upToNextMajor— this decision should be documented. - Configuration type:
api,implementation,compileOnly,ksp— which one, and why. - Play/App Store policy compliance: on Android, a manual pre-check of the four criteria (outdated/policy/vulnerability/deprecated) that Android Studio scans (via the Google Play SDK Index).
- Abstraction: is the SDK called directly, or is it behind a protocol/interface.
Wiring automatic-update and security warnings into CI
On the Android side this integration is already built in: AGP embeds the dependency metadata it reads when you build the app into the APK/AAB, and "When uploading your app, the Play Console inspects this metadata to provide alerts for known issues with SDKs and dependencies your app uses." So without setting up a separate CI step, the app-upload step itself turns into a security checkpoint.
On the GitHub side, the most practical way to wire SBOM into CI is to move the export flow into a workflow. The skeleton below wires SBOM generation into the pull-request flow (the step name and the uses: line should be adapted to your repo — use the action your own organization has approved):
1# .github/workflows/dependency-audit.yml — skeleton2name: Dependency Audit3on:4 pull_request:5 paths:6 - "**/build.gradle.kts"7 - "**/Package.swift"8 - "**/Podfile"9jobs:10 audit:11 runs-on: ubuntu-latest12 steps:13 - uses: actions/checkout@v414 - name: Flag a new dependency change15 run: |16 echo "This PR modifies a manifest file — apply the acceptance criteria checklist."The real job of this skeleton is to automate the "did the manifest change" question and remind the reviewer — generating the SBOM itself still needs GitHub's own export flow (Dependency graph → Export SBOM) or an approved SBOM action.
Response plan for a compromised-package scenario
In an upstream package-compromise scenario, the first thing you should ask is: "How did I pin this dependency?" When a dependency is locked to the commit-hash level with revision(_:) in SPM — in the documentation's words, "Returns a requirement for a source control revision such as the hash of a commit." — even if the upstream repo is compromised and a malicious version is published, your project stays locked to that commit and won't automatically pull that version. This is the technical foundation of the response plan: revision-pinning blocks "unexpected update" risk at the API level.
The reverse is also true: a dependency added with .upToNextMajor may be pulled automatically on the next build if a malicious minor/patch version is published upstream. Your response plan should include these steps:
- Detection: set up a channel that watches Play Console's known-SDK-issue alerts, Android Studio's SDK Index lint findings, or the warning GitHub generates.
- Freeze: immediately lock the affected dependency to a known-clean version/commit with
.exactor.revision. - Scoping: use the transitive-path information in the SBOM to determine which modules use this package (the SBOM doesn't include "dependents," i.e. it doesn't show who uses your project, only what your project uses — scoping is done within your own repo(s)).
- Communication: if you're on a modular architecture (see Clean Architecture iOS), isolate the affected layer and let the patch land without blocking other modules' builds.
Quarterly audit template
The four criteria Android Studio already applies — "The SDKs are marked as outdated by their authors, The SDKs violate Play policies, The SDKs have known security vulnerabilities, The SDKs have been deprecated by their authors." — form the backbone of a periodic audit template. Repeat the following list for every module once a quarter:
- How many major versions behind the latest release is each dependency; for packages pinned with
.exact/.revision, is this gap a deliberate delay or a forgotten one? - Is none of the outdated/policy/vulnerability/deprecated findings flagged by Android Studio (via the SDK Index) or by build warnings left open unresolved?
- When you re-run the GitHub SBOM export and compare it against the previous quarter, has an unexpected new transitive package appeared?
- For targets using CocoaPods (which has no official inventory tool), has
Podfile.lockbeen reviewed manually or with a third-party scanner?
GOLDEN TIP
The most valuable insight in this article
This tip holds the article's most important takeaway.
Easter Egg
You found a hidden gem!
There's a hidden detail in this section. Want to uncover it?
Reader Reward
I've condensed every official mechanism covered in this article into a single checklist — a step-by-step, actionable list showing how to use the tools the three platforms (Gradle, SPM, GitHub) already give you, at no extra cost. Applying the items below in order will make most of the dependency chain's invisible surface visible.
FAQ
How do you extract a dependency inventory in a mobile app?
The starting point of the inventory varies by platform: in Gradle it's the configuration types (api, implementation, compileOnly, ksp) in build.gradle.kts; in SPM it's the dependencies: [Package.Dependency] list in Package.swift; in CocoaPods it's the Podfile. If you're on GitHub, you can extract a formal inventory that also includes transitive paths via an SBOM export from the Dependency graph under the Insights tab — but remember this SBOM doesn't include dependents (who uses you).
What is an SBOM and how is it generated on mobile?
An SBOM (Software Bill of Materials) is a document that lists a project's dependency inventory in an industry-standard format (SPDX); it includes version, package identifier, license, transitive path and copyright information. On GitHub you can generate it via the repo's Insights tab → Dependency graph → Export SBOM flow, or in SPDX 2.2-compliant form via GitHub Actions. Since there's no official CocoaPods-specific SBOM tool, you need third-party scanners for CocoaPods targets.
What should you do if a dependency is compromised?
First look at how the dependency was pinned: a package locked to a commit hash with .revision(_:) won't be pulled automatically even if a malicious version is published upstream. If the pinning is loose (.upToNextMajor or a dynamic version in Gradle), immediately freeze the affected package to a known-clean version/commit, use the transitive-path information in the SBOM to identify the affected modules, and if you're on a modular architecture, isolate the affected layer without blocking other modules' builds.
In Gradle, which dependency should be added with which configuration?
If a dependency is only used within the module itself, use implementation; if consumer modules also need access to it, use api; if it's only needed on the compile classpath (won't enter runtime), use compileOnly; if it generates code at compile time, use ksp/kapt/annotationProcessor. This choice directly determines the accuracy of your inventory.
What are the version-pinning options in SPM?
package(url:exact:), package(url:from:) and package(url:revision:) offer three basic options: exact locks to an exact version, from (upToNextMajor) defines a controlled range bound to the major version number, and revision pins to a commit hash or revision identifier (the legacy Requirement enum behind these calls has been deprecated since SwiftPM 5.6).
Update (September 2026)
This article was written on 2025-11-13, based on the official documentation available at that time. Since then, two developments are directly relevant: under the EU's Cyber Resilience Act, the obligation for manufacturers shipping products/SDKs to the EU market to report actively exploited vulnerabilities took effect on September 11, 2026 — showing that the dependency inventory (SBOM) has moved from an optional good practice to a legal pressure.
Source: Cyber Resilience Act — European Commission.
There's one more signal on the attack-surface side: npm's infrastructure and its mirrors were exploited in a phishing campaign in August 2026 — not a mobile-specific incident, but it confirms that the supply chain has become an independent security discipline in its own right.
Source: npm mirror phishing — BleepingComputer.
Conclusion
Mobile dependency chain security is built from three official mechanisms: Gradle's configuration-type distinction plus Play Console's metadata scanning, SPM's version/commit-level pinning via package(url:exact:) / package(url:from:) / package(url:revision:), and GitHub's SPDX-format SBOM export. No official CocoaPods inventory tool exists, so manual discipline is mandatory there.
If you've built a modular architecture (Modular iOS Architecture and Swift Package Manager, SPM Advanced Modular), combining your new-dependency acceptance criteria with module boundaries is a natural next step. Combine general security practices (iOS Security Best Practices) and your sensitive-data storage strategy (iOS Keychain Security) with this dependency discipline, and you close off a large part of the attack surface. Placing third-party SDKs behind an abstraction layer (Swift Dependency Injection) instead of injecting them directly as concrete types also speeds up your response in a compromise scenario — especially in projects built on a clean layer separation (Clean Architecture iOS).
Sources
- Android build dependencies — the official source for Gradle configuration types, transitive resolution, Play Console metadata scanning and the dynamic-version warning.
- PackageDescription: Package — the page defining the Swift Package Manager's
dependenciesfield and the.package(url:from:)example. - PackageDescription: Package.Dependency — the official type definition of a package dependency.
- PackageDescription: Requirement enum — the official source for the
exact,upToNextMajor,upToNextMinor,revisionpinning options (deprecated since SwiftPM 5.6; current usage is the labeled calls such aspackage(url:exact:)). - GitHub Supply Chain Security — the hub page for supply-chain security documentation.
- GitHub SBOM export guide — the SBOM's content, UI flow and generation steps via Actions.
- Cyber Resilience Act — the EU's vulnerability-reporting obligation that took effect on September 11, 2026.
- BleepingComputer: npm mirror phishing — a recent case of supply-chain infrastructure being exploited.
Tags
iOS Development News
Weekly Swift tips, SwiftUI tricks and iOS best practices. No spam, only valuable content.
We respect your privacy. You can unsubscribe at any time.

