[Pitch] Software Bill of Materials (SBOM) Generation for Swift Package Manager

Introduction

An SBOM (Software Bill of Materials) provides a detailed inventory of software components included in an artifact. SBOMs allow developers to improve and analyze the software supply chain security profile for their Swift projects (for example, determining whether a dependency that's being used has a vulnerability). Also, some companies, governments, and other regulatory bodies require SBOMs to be produced for auditing purposes.

There are two common formats for SBOMs: CycloneDX and SPDX.

Motivation

Currently, Swift Package Manager lacks built-in support for generating SBOMs. Instead, developers have to rely on external or third-party tools to create SBOMs. External and third-party tools usually analyze the Package.swift and Package.resolved files for package dependencies, so information about what product uses which dependencies is missing.

Proposed Solution

This pitch describes adding CycloneDX and SPDX SBOM generation capabilities to Swift Package Manager as part of the build command and as a separate package subcommand.

Integrated Build Command

swift build will take an optional flag --sbom-spec that triggers CycloneDX and/or SPDX SBOM generation as part of the build command. SwiftPM will analyze the resolved package graph and optionally the SwiftBuild build system’s dependency graph, and produce SBOMs for the root package or product.

Re-running the command will not overwrite previously generated SBOMs; timestamps will be appended to SBOMs. This is to address the case when the same product or package is intentionally built multiple times with different flags; all SBOMs (not just the most recent) are relevant to the user.

If SBOM generation fails, the build will return an error.

Traits and Conditions

Dependencies affected by traits are taken into consideration during package resolution. SwiftPM’s resolved package graph will already reflect the impact of traits on dependencies.

Conditions (e.g., OS-specific dependencies) are evaluated at build-time. SwiftPM’s package graph does not reflect conditions. Instead, the SBOM command will look at SwiftBuild build system’s computed dependency graph to determine which dependencies from the package graph should be included or excluded in the final SBOMs.

Incremental Builds

SBOM generation is not incremental, so if SBOM generation is specified, the build will always produce new SBOMs, even if the artifacts have not changed since the last build.

SBOM generation does not affect whether an artifact build will be full or incremental.

CLI Examples

A user can run the following commands:

# To generate CycloneDX SBOM file for a package using both package graph and build graph
$ swift build --build-system swiftbuild --sbom-spec cyclonedx

# To generate SPDX SBOM file for a package using both package graph and build graph
$ swift build --build-system swiftbuild --sbom-spec spdx

# To generate CycloneDX and SPDX SBOM files for a package using only the package graph
$ swift build --sbom-spec cyclonedx --sbom-spec spdx

# To generate a CycloneDX or SPDX SBOM file for a single product using both package graph and build graph
$ swift build --build-system swiftbuild --product MyProduct1 --sbom-spec cyclonedx
$ swift build --build-system swiftbuild --product MyProduct2 --sbom-spec spdx

# To generate SBOM files into a specific directory
$ swift build --build-system swiftbuild --sbom-spec cyclonedx --sbom-spec spdx --sbom-dir /MyDirectory

Optional Flags

--sbom-spec will trigger SBOM generation when a build is run. Either cyclonedx or spdx or both can be passed to indicate what kind of SBOMs to generate.

--sbom-spec can only be used with one of the following:

  • without --product and without --target flags (i.e., a SBOM for the entire package), or
  • with --product flag (i.e., a SBOM for a specific product)

The build will error if --sbom-spec is used with the --target flag.

If --build-system swiftbuild is not specified, a warning will be emitted that only the package graph is being used for SBOM generation. (The build dependency graph is only available through SwiftBuild.)

cyclonedx and spdx flags will always point to the most recently SwiftPM-supported major versions, but users have the option to specify the major version they'd like.

OPTIONS:
  --sbom-spec <spec>           Set the SBOM specification.
        cyclonedx         - Most recent major version of CycloneDX supported by SwiftPM (currently: 1.7)
        spdx              - Most recent major version of SPDX supported by SwiftPM (currently: 3.0.1)
        cyclonedx1        - Most recent minor version of CycloneDX v1 supported by SwiftPM  (currently: 1.7)
        spdx3             - Most recent minor version of SPDX v3 supported by SwiftPM  (currently: 3.0.1)
        # Future: cyclonedx2...
        # Future: spdx4...

Additionally, users can specify the following optional flags (which must appear with --sbom-spec):

--sbom-dir <sbom-dir>  The absolute or relative directory path to generate the SBOM(s) in.
--sbom-filter <filter> Filter the SBOM components and dependencies by products and/or packages.
        all               - Include all entities in the SBOM (default)
        package           - Only include package information and package dependencies
        product           - Only include product information and product dependencies

Configuration

Environment variables can be used for SBOM generation configuration in the short-term.

An issue in Github will be raised to address changing SBOM environment variables to a configuration file.

  • SWIFTPM_BUILD_SBOM_DIRECTORY: specifies which directory the SBOMs should be generated in. If --sbom-dir is passed to swift build, --sbom-dir will take precedence.
  • SWIFTPM_BUILD_SBOM_FILTER: specifies which filter to apply, defaults to all. If --sbom-filter is passed to swift build, --sbom-filter will take precedence.
  • (Needs further discussion) SWIFTPM_BUILD_SBOM_WITHOUT_FAILURE: automatically generates both CycloneDX and SPDX SBOMs as part of the swift build command. If --sbom-spec is passed to swift build, --sbom-spec will take precedence. A failure in SBOM generation will not cause the build to fail.
    • This addresses the use case where an external party (e.g., infrastructure or security teams) wants to generate SBOMs for all Swift projects under their purview without interfering with build results.
    • Alternatives include:
      • A specific exit code to use when the build passes but SBOM generation fails (e.g., permission to write to SBOM folder is denied). However, this would cause most CI pipelines to still fail, unless an exception is built into the CI system, so this is not ideal for most infrastructure and security teams.
      • An additional environment variable SWIFTPM_BUILD_SBOM_WITH_FAILURE that automatically generates CycloneDX and SPDX SBOMs as part of the swift build command. A failure in SBOM generation will cause the build to fail (i.e., default behavior, but without changing the swift build command that teams have written).
        • For example, if a team is using the --target flag, the build will pass, but the build exit code will be a failure code because SBOM generation does not work with --target.
      • Two environment variables, SWIFTPM_BUILD_SBOM (default: false) and SWIFTPM_SBOM_RECORD_FAILURE (default: true) that toggle whether to build SBOMs automatically and whether to fail the build if SBOM generation fails. This would also necessitate adding a --sbom-record-failure flag to the CLI, so that CLI and environment variables match.

Package Subcommand

SBOM generation will also be added as a separate subcommand swift package generate-sbom. It will share the same SBOM flags as SBOM generation for swift build. Unlike SBOM generation in swift build, swift package generate-sbom will not call the build or use the SwiftBuild build dependency graph.

This subcommand is to address use cases where an SBOM might need to be created after a build (for example, in a CICD pipeline or using a different version of the toolchain), but calling swift build again is undesirable or impossible.

The subcommand will always emit a warning that the SBOM may not be fully accurate because it only uses the package graph. The user will have the option to pass --disable-automatic-resolution to force SBOM generation to fail if the Package.resolved is not up-to-date.

Note: Using the package subcommand can result in dependency resolution timing issues. There is an edge case where an artifact is built, the dependency graph changes, and then the SBOM is generated. In that case, the SBOM will not accurately reflect the built artifact’s components and dependencies.

CLI Examples

A user can run the following commands:

# To generate CycloneDX SBOM file for a package using the package graph
$ swift package generate-sbom --sbom-spec cyclonedx

# To generate SPDX SBOM file for a package using the package graph
$ swift package generate-sbom --sbom-spec spdx

# To generate CycloneDX and SPDX SBOM files for a package using the package graph
$ swift package generate-sbom --sbom-spec cyclonedx --sbom-spec spdx

# To generate a CycloneDX or SPDX SBOM file for a single product using the package graph
$ swift package generate-sbom --product MyProduct1 --sbom-spec cyclonedx
$ swift package generate-sbom --product MyProduct2 --sbom-spec spdx

# To generate SBOM files into a specific directory
$ swift package generate-sbom --sbom-spec cyclonedx --sbom-spec spdx --sbom-dir /MyDirectory

Design

The SBOM generation will have three layers:

  1. Extractor: Reads information from the package graph and build dependency graph and saves it into internal data structures
  2. Converters: Read information from internal data structures and convert them into CycloneDX and SPDX equivalent structures
  3. Validator: Reads CycloneDX and SPDX SBOM JSON objects and validates them against the appropriate schema

SBOM Content

This feature will support the most recent minor versions of major versions starting with CycloneDX 1 and SPDX 3.

Only JSON files will be generated. JSON is a common file format supported by both CycloneDX and SPDX. JSON is the preferred format for CycloneDX and becoming more common in SPDX.

Generated SBOMs will include:

Components

  • Component CycloneDX type or package SPDX purpose; name; and version
  • Package URL (PURL) for unique identification
  • Source repository information

CycloneDX:

{
    "bom-ref" : "swift-asn1:SwiftASN1",
    "name" : "SwiftASN1",
    "pedigree" : {
        "commits" : [
            {
                 "uid" : "40d25bbb2fc5b557a9aa8512210bded327c0f60d",
                 "url" : "https://github.com/apple/swift-asn1.git"
            }
        ]
    },
    "properties" : [
        {
            "name" : "swift-entity",
            "value" : "swift-product"
        }
    ],
    "purl" : "pkg:swift/github.com/apple/swift-asn1:SwiftASN1@1.5.0",
    "scope" : "required",
    "type" : "library",
    "version" : "1.5.0"
}

SPDX:

{
    "creationInfo" : "_:creationInfo",
    "description" : "<ResolvedPackage: swift-asn1>",
    "externalUrl" : "pkg:swift/github.com/apple/swift-asn1@1.5.0",
    "name" : "swift-asn1",
    "software_internalVersion" : "1.5.0",
    "software_primaryPurpose" : "library",
    "spdxId" : "urn:spdx:swift-asn1",
    "summary" : "swift-package",
    "type" : "software_Package"
},
{
    "creationInfo" : "_:creationInfo",
    "from" : "urn:spdx:40d25bbb2fc5b557a9aa8512210bded327c0f60d",
    "relationshipType" : "generates",
    "spdxId" : "urn:spdx:40d25bbb2fc5b557a9aa8512210bded327c0f60d-generates",
    "to" : [
        "urn:spdx:swift-asn1",
        "urn:spdx:swift-asn1:SwiftASN1"
    ],
    "type" : "Relationship"
},
{
    "externalIdentifierType" : "gitoid",
    "identifier" : "urn:spdx:40d25bbb2fc5b557a9aa8512210bded327c0f60d",
    "identifierLocator" : [
        "https://github.com/apple/swift-asn1.git"
    ],
    "type" : "ExternalIdentifier"
}

Dependencies

  • Direct and transitive relationships
  • Package-to-package dependencies (only packages used by the root package or specified product)
  • Package-to-product dependencies (i.e., a package depends on its own products)
  • Product-to-product dependencies (only products used by the root package or specified product)

CycloneDX:

{
    "dependsOn" : [
        "swift-asn1:SwiftASN1"
    ],
    "ref" : "swift-crypto:_CryptoExtras"
},
{
    "dependsOn" : [
        "swift-crypto:Crypto",
        "swift-crypto:_CryptoExtras",
        "swift-asn1"
    ],
    "ref" : "swift-crypto"
},
{
    "dependsOn" : [
        "swift-asn1",
        "swift-crypto",
        "swift-certificates:X509"
    ],
    "ref" : "swift-certificates"
},
{
    "dependsOn" : [
        "swift-crypto:Crypto",
        "swift-asn1:SwiftASN1",
        "swift-crypto:_CryptoExtras"
    ],
    "ref" : "swift-certificates:X509"
},
{
    "dependsOn" : [
        "swift-asn1:SwiftASN1"
    ],
    "ref" : "swift-asn1"
}

SPDX:

{
    "creationInfo" : "_:creationInfo",
    "from" : "urn:spdx:swift-crypto:_CryptoExtras",
    "relationshipType" : "dependsOn",
    "spdxId" : "urn:spdx:swift-crypto:_CryptoExtras-dependsOn",
    "to" : [
        "urn:spdx:swift-asn1:SwiftASN1"
    ],
    "type" : "Relationship"
},
{
    "creationInfo" : "_:creationInfo",
    "from" : "urn:spdx:swift-crypto",
    "relationshipType" : "dependsOn",
    "spdxId" : "urn:spdx:swift-crypto-dependsOn",
    "to" : [
        "urn:spdx:swift-crypto:Crypto",
        "urn:spdx:swift-crypto:_CryptoExtras",
        "urn:spdx:swift-asn1"
    ],
    "type" : "Relationship"
},
{
    "creationInfo" : "_:creationInfo",
    "from" : "urn:spdx:swift-certificates",
    "relationshipType" : "dependsOn",
    "spdxId" : "urn:spdx:swift-certificates-dependsOn",
    "to" : [
        "urn:spdx:swift-asn1",
        "urn:spdx:swift-crypto",
        "urn:spdx:swift-certificates:X509"
    ],
    "type" : "Relationship"
},
{
    "creationInfo" : "_:creationInfo",
    "from" : "urn:spdx:swift-certificates:X509",
    "relationshipType" : "dependsOn",
    "spdxId" : "urn:spdx:swift-certificates:X509-dependsOn",
    "to" : [
        "urn:spdx:swift-crypto:Crypto",
        "urn:spdx:swift-asn1:SwiftASN1",
        "urn:spdx:swift-crypto:_CryptoExtras"
    ],
    "type" : "Relationship"
},
{
    "creationInfo" : "_:creationInfo",
    "from" : "urn:spdx:swift-asn1",
    "relationshipType" : "dependsOn",
    "spdxId" : "urn:spdx:swift-asn1-dependsOn",
    "to" : [
        "urn:spdx:swift-asn1:SwiftASN1"
    ],
    "type" : "Relationship"
}

Metadata

  • SwiftPM version used
  • SBOM timestamp
  • Spec version

CycloneDX:

"metadata" : {
...
    "timestamp" : "2025-11-19T21:42:50Z",
    "tools" : {
        "components" : [
            {
                "bom-ref" : "urn:uuid:40316357-938f-4a8c-9962-e928fbc251a6",
                "licenses" : [
                    {
                        "license" : {
                            "id" : "Apache-2.0",
                            "url" : "http://swift.org/LICENSE.txt"
                         }
                    }
                ],
                "name" : "swift-package-manager",
                "purl" : "pkg:swift/github.com/swiftlang/swift-package-manager@6.3.0-dev",
                "scope" : "excluded",
                "type" : "application",
                "version" : "6.3.0-dev"
            }
        ]
    }
},
"serialNumber" : "urn:uuid:8318774a-f646-4eab-a08c-0dc3f952abc5",
"specVersion" : "1.7",
"version" : 1

SPDX:

{
    "creationInfo" : "urn:uuid:40316357-938f-4a8c-9962-e928fbc251a6:creationInfo",
    "from" : "urn:uuid:40316357-938f-4a8c-9962-e928fbc251a6",
    "relationshipType" : "hasDeclaredLicense",
    "spdxId" : "urn:uuid:40316357-938f-4a8c-9962-e928fbc251a6-hasDeclaredLicense-urn:spdx:Apache-2.0",
    "to" : [
        "urn:spdx:Apache-2.0"
    ],
    "type" : "Relationship"
},
{
    "creationInfo" : "urn:uuid:40316357-938f-4a8c-9962-e928fbc251a6:creationInfo",
    "simplelicensing_licenseExpression" : "Apache-2.0",
    "spdxId" : "urn:spdx:Apache-2.0",
    "type" : "simplelicensing_LicenseExpression"
},
{
    "@id" : "urn:uuid:40316357-938f-4a8c-9962-e928fbc251a6:creationInfo",
    "created" : "1970-01-01T00:00:00Z",
    "createdBy" : [
        "urn:uuid:40316357-938f-4a8c-9962-e928fbc251a6"
    ],
    "specVersion" : "6.3.0-dev",
    "type" : "CreationInfo"
},
{
    "creationInfo" : "urn:uuid:40316357-938f-4a8c-9962-e928fbc251a6:creationInfo",
    "name" : "swift-package-manager",
    "spdxId" : "urn:uuid:40316357-938f-4a8c-9962-e928fbc251a6",
    "type" : "Agent"
},
{
    "@id" : "_:creationInfo",
    "created" : "2025-11-19T21:42:50Z",
    "createdBy" : [
        "urn:uuid:40316357-938f-4a8c-9962-e928fbc251a6"
    ],
    "specVersion" : "3.0.1",
    "type" : "CreationInfo"
},
{
    "creationInfo" : "_:creationInfo",
    "profileConformance" : [
        "core",
        "software"
    ],
    "rootElement" : [
        "urn:spdx:swift-package-manager"
    ],
    "spdxId" : "urn:uuid:5aee3149-5395-4a41-a076-2491437026d3",
    "type" : "software_Sbom"
}

Primary Component

CycloneDX:

"component" : {
    "bom-ref" : "swift-package-manager",
    "name" : "swift-package-manager",
    "pedigree" : {
        "commits" : [
            {
                "uid" : "37990426e3f1cb4344f39641e634c76130c1fb42",
                "url" : "git@github.com:echeng3805/swift-package-manager.git"
            }
        ]
    },
    "properties" : [
        {
            "name" : "swift-entity",
            "value" : "swift-package"
        }
    ],
    "purl" : "pkg:swift/github.com/echeng3805/swift-package-manager@37990426e3f1cb4344f39641e634c76130c1fb42-modified",
    "scope" : "required",
    "type" : "application",
    "version" : "37990426e3f1cb4344f39641e634c76130c1fb42-modified"
}

SPDX:

{
    "creationInfo" : "_:creationInfo",
    "description" : "<ResolvedPackage: swift-package-manager>",
    "externalUrl" : "pkg:swift/github.com/swiftlang/swift-package-manager@37990426e3f1cb4344f39641e634c76130c1fb42-modified",
    "name" : "swift-package-manager",
    "software_internalVersion" : "37990426e3f1cb4344f39641e634c76130c1fb42-modified",
    "software_primaryPurpose" : "application",
    "spdxId" : "urn:spdx:swift-package-manager",
    "summary" : "swift-package",
    "type" : "software_Package"
}

Security

This will strengthen Swift’s supply chain security profile by providing a basic inventory for packages and products.

Impact on Existing Packages

This feature doesn't modify existing SwiftPM functionality, like the build graph or dependency resolution. It adds an optional feature to the build command and a new package subcommand.

Alternatives Considered

Build Plugin

Build plugins are meant to generate auxiliary build artifacts. An SBOM isn’t really a build artifact; it’s metadata about build artifacts.

Command Plugin

Command plugins need to be added to the Package.swift file to be available. Building SBOM generation directly into swift build gives users the feature without any need to change existing Package.swift files or existing configuration.

Future Features

Some future features that can be added include:

  • Best attempt at licenses: Automatic license identification from source code (some edge cases make it difficult to exactly accurately determine licenses; examples: multi-license projects depending on whether payment was rendered, multiple licenses in the repository, license of single file vs license of whole project, changes in license verbiage without changing the name of the license, relicensing for different versions of the same project)
  • --target flag support: Support more granular SBOMs (per target)
  • Additional information: Maintainers’ contact information, commit/forking history, additional build metadata (e.g., host, operating system)
  • Additional file formats: XML, TV, YAML. XML is a format supported by both specs; however, XML is not the preferred format for either CycloneDX or SPDX.
  • Additional spec versions: CycloneDX 2, SPDX 4 when available
  • Merged SBOMs: Generate one SBOM for multiple products/targets, merge or link existing SBOM (e.g., from a third-party SDK) into output SBOM
  • Independent CycloneDX and SPDX libraries: For parsing CycloneDX/SPDX files, or supporting other fields besides the bare minimum
  • Package.resolved generation: Generate a Package.resolved file based on an SBOM in order to reproduce a dependency graph (e.g., for debugging)
  • SBOM signing: Sign the SBOM cryptographically to link it to an artifact
  • Hashes: Add hashes to the SBOM
24 Likes

This doesn’t seem accurate to me, the way things are proposed is that the SBOM can literally be another output of swift build which by definition is a build artifact. I would just merge this with the justification for not using command plugins below since the same considerations would apply.

1 Like