Tracing Dependencies and Generating Dependency Graphs

I'm looking at gathering dependency graphs for builds that use Swift Package Manager. I'd like to know what dependencies went into a build so they can be tracked for security reasons. This is similar to how a tool like Xray collects dependency information for a build, and then can watch for known exploits on library dependencies.

I'm finding there isn't a great way to collect what dependencies went into a build:

  • Swift Package's "show-dependencies" subcommand works great, but it only works on Swift packages. I don't think there is a corresponding option for xcodebuild when the final build is an Xcode project. I'm also not sure how archivable or parsable this output is (especially if xcodebuild introduces it's own version with different output.)
  • The manifest file also seems like a good option. Both Xcode and Swift packages maintain this file. But it only captures (I think) dependency state one level deep. If project A depends on library B, and library B depends on library C, only the state of library B is captured. I don't know what version of library C was used for the build.

Has anyone else found a good method or tooling for archiving the state of dependencies for a build?

2 Likes

The Package.resolved file should work for this.

1 Like

Thanks, sorry, I realized in my original post I used the term "manifest" when I really meant the Package.resolved file.

The issue with Package.resolved I'm seeing is it seems to only capture dependencies one level deep. So if I depend on LibraryA, and LibraryA depends on LibraryB, I won't capture what version of LibraryB was used.

At least that seems to be how it's behaving in the few projects I have with multi level dependencies.

1 Like

swift package show-dependencies shows recursive dependencies. For example, here's the output for the dealer example package.

$ swift package show-dependencies --format json
{
  "name": "dealer",
  "url": "https://github.com/apple/example-package-dealer",
  "version": "unspecified",
  "path": "example-package-dealer",
  "dependencies": [
    {
      "name": "DeckOfPlayingCards",
      "url": "https://github.com/apple/example-package-deckofplayingcards.git",
      "version": "3.0.4",
      "path": ".build/checkouts/example-package-deckofplayingcards",
      "dependencies": [
        {
          "name": "FisherYates",
          "url": "https://github.com/apple/example-package-fisheryates.git",
          "version": "2.0.6",
          "path": ".build/checkouts/example-package-fisheryates",
          "dependencies": [

          ]
        },
        {
          "name": "PlayingCard",
          "url": "https://github.com/apple/example-package-playingcard.git",
          "version": "3.0.5",
          "path": ".build/checkouts/example-package-playingcard",
          "dependencies": [

          ]
        }
      ]
    }
  ]
}

But like you said, this is incomplete, as it only includes Swift dependencies.

I recently used that to generate a software bill of materials for Swift projects in spdx-sbom-generator. With the latest Executive Order on Cybersecurity, there's a lot of work being done on SBOMs, which have to do with the traceability part of what you're looking for.

Beyond that, my role at GitHub is focused on improving the experience for Swift developers across the platform, including integration with Dependabot, the Security Advisories, and the dependency graph. Feel free to reach out and let me know more about your use case, and how we might work together to improve tooling across the Swift ecosystem. :smiley:

3 Likes

Thanks. This looks like a helpful start. I don't think there is a way to run the command against xcodeprojs that use Swift dependencies. But this data is exactly the sort of thing I'm looking for.

The concerns I'm dealing with are security related as well. But the Swift tooling is not quite where other dependency manages like Nuget and NPM are at. (Which is fine, Swift is pretty young!)

Right now my interest is mostly in capturing data that could be used to manually trace dependencies, in since automated solutions don't exist yet. I'd love to look at tooling solutions and enhancing what's available for Swift! But I'm still at the point where I'm churning through a lot of existing tooling and figuring out integration points.

I have been watching the work that you and Github have been done. Package Registries seems also helpful for this use case. Unfortunately the use case I'm looking at is a binary distribution model, so I haven't been able to take advantage of most of those enhancements yet. I'd love to be able to push our vendors, at least a bit, to start adopting support for SPM Package Registries. :grinning_face_with_smiling_eyes:

I think on this question, if there isn't a way to pass "swift package" commands through xcodebuild, I probably need to open a radar with Apple to support the dependency graph output.

1 Like

That sounds like a bug to me, it should be capturing the entire dependency graph. Could you share an example where that isn't working?

I might see why it's not working as expected. I can't copy and paste what I have in front of me, but I can probably explain it as a hypothetical.

Let's say I have a dependency tree that looks like this:

Project X -> Package A
          -> Package B
          -> Package C -> Swift Argument Parser

In this case, Package C has a checked in Package.resolved making it's relationship with the argument parser explicit. Project X's Package.resolved doesn't include a pin for Swift Argument Parser. But my assumption is that it probably shouldn't. It's going to satisfy the pin using what was was checked in for Package C.

If you wanted a report of your dependency tree, this is maybe still an issue. You still can't capture the Package.resolved file and have a complete picture of all your dependencies. But in since Package.resolved is checked in for Package C, you could still gather a complete understanding by visiting all the packages at their checkin points. So no information is being lost.

Does this behavior sound correct? If not, I can keep pulling at it and put together a sample project. But to me, it makes sense why SPM wouldn't include the Swift Argument Parser pin in the root Project X Package.resolved file.

Resolved files of dependencies aren't actually taken into account, only the one of the root package (or the one at the workspace-level if an Xcode project or workspace is involved).

So I still think you must be hitting a bug.

1 Like

I'll put together a public facing repro case. I'd hate to cause a fuss over me misunderstanding how this is supposed to work based on a case I can't share. That would also help eliminate variables like my project and Xcode's SPM integration. Should have something up in the next few days.

I spent some time testing different iterations of dependency trees. It looks like the base Package.resolved will include the entire tree of dependencies for library. But it won't include portions of the tree that only an executable declares a dependency on.

A good example is swift-collections. If I include Swift collections in my project, the dependency on swift-collections-benchmark will not be tracked by the base Package.resolved file. This is because only the benchmark executable actually declares a need for swift-collections-benchmark, and not the library itself.

In my case, I had dependencies that declared executables that depended on swift-argument-parser in my workspace, but swift-argument-parser did not show up in my workspaces Package.resolved file.

It's a narrower edge case. But it does still feel like it could be important. If I shipped executables from Swift packages, it would still be helpful to capture the entire dependency tree.

Ah, what you're seeing is likely the effect of SE-0226. I would assume if an executable is explicitly declared as a product, it'll be represented in the resolved file.

Looks like Build Graph for Xcode solves this question

Sort of. Unfortunately the build graphs only surface relative timing information. There’s no dependency analysis or really any detail at all made available. Unless your bottleneck is blindingly obvious, the graph really can’t help you find it.

It also contains info about links between modules. It can be shown by moving pointer over a module