Skip to content

Add Swift Package Manager parser for Package.swift/resolved#139

Merged
anderruiz merged 20 commits into
mainfrom
ander/shift-package-manager
May 6, 2026
Merged

Add Swift Package Manager parser for Package.swift/resolved#139
anderruiz merged 20 commits into
mainfrom
ander/shift-package-manager

Conversation

@anderruiz

@anderruiz anderruiz commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a Swift Package Manager parser that handles all three Package.resolved lockfile formats (v1, v2, v3) and a Package.swift matcher for direct-dependency enrichment.

  • Parses Package.resolved formats:
    • v1 (Xcode 11): object.pins[] with repositoryURL
    • v2 (Xcode 13): pins[] with location
    • v3 (Xcode 15.3): same as v2 plus originHash
  • Skips localSourceControl pins (no meaningful purl URL)
  • All packages default to IsDirect=false, LocationRole=lockfile
  • Package.swift matcher enriches direct dependencies: sets IsDirect=true, LocationRole=manifest, and BlockLocation pointing to the .package(url:...) declaration line
  • Matcher uses regex-based paren-depth parsing to handle multi-line .package(...) declarations
  • purl format: pkg:swift/github.com/{owner}/{repo}@{version}
{"$schema":"http://cyclonedx.org/schema/bom-1.5.schema.json","bomFormat":"CycloneDX","specVersion":"1.5","version":1,"metadata":{"tools":{"components":[{"type":"application","group":"datadog","name":"datadog-sbom-generator","version":"set at build time, see .goreleaser.yml ldflags section"}]}},"components":[{"bom-ref":"pkg:swift/github.com/apple/swift-argument-parser@1.6.2","type":"library","name":"github.com/apple/swift-argument-parser","version":"1.6.2","purl":"pkg:swift/github.com/apple/swift-argument-parser@1.6.2","properties":[{"name":"datadog:is-direct","value":"true"},{"name":"datadog:package-manager","value":"SwiftPM"}],"evidence":{"occurrences":[{"location":"{\"block\":{\"file_name\":\"Package.swift\",\"line_start\":176,\"line_end\":176,\"column_start\":5,\"column_end\":88,\"role\":\"manifest\"}}"}]}},{"bom-ref":"pkg:swift/github.com/swiftlang/swift-llbuild","type":"library","name":"github.com/swiftlang/swift-llbuild","purl":"pkg:swift/github.com/swiftlang/swift-llbuild","properties":[{"name":"datadog:is-direct","value":"true"},{"name":"datadog:package-manager","value":"SwiftPM"}],"evidence":{"occurrences":[{"location":"{\"block\":{\"file_name\":\"Package.swift\",\"line_start\":153,\"line_end\":153,\"column_start\":13,\"column_end\":93,\"role\":\"manifest\"}}"}]}},{"bom-ref":"pkg:swift/github.com/swiftlang/swift-toolchain-sqlite@1.0.7","type":"library","name":"github.com/swiftlang/swift-toolchain-sqlite","version":"1.0.7","purl":"pkg:swift/github.com/swiftlang/swift-toolchain-sqlite@1.0.7","properties":[{"name":"datadog:package-manager","value":"SwiftPM"}]},{"bom-ref":"pkg:swift/github.com/swiftlang/swift-tools-support-core","type":"library","name":"github.com/swiftlang/swift-tools-support-core","purl":"pkg:swift/github.com/swiftlang/swift-tools-support-core","properties":[{"name":"datadog:is-direct","value":"true"},{"name":"datadog:package-manager","value":"SwiftPM"}],"evidence":{"occurrences":[{"location":"{\"block\":{\"file_name\":\"Package.swift\",\"line_start\":172,\"line_end\":172,\"column_start\":5,\"column_end\":96,\"role\":\"manifest\"}}"}]}}]}

New files

File Purpose
pkg/lockfile/swift/types.go Structs for v1/v2/v3 JSON formats
pkg/lockfile/swift/parse-package-resolved.go Extractor + init() registration
pkg/lockfile/swift/match-package-swift.go Matcher for Package.swift
internal/utility/purl/swift.go purl generation for Swift packages

Test plan

  • Unit tests for extractor: v1, v2, v3 formats, local packages skipped, malformed input
  • Unit tests for matcher: single/multi-line declarations, unmatched packages, multiline fixture
  • Unit tests for purl generation
  • Registered in pkg/lockfile/parsers/parsers.go
  • All tests passing

🤖 Generated with Claude Code

@anderruiz anderruiz requested a review from a team as a code owner April 29, 2026 12:15
@datadog-prod-us1-5

datadog-prod-us1-5 Bot commented Apr 29, 2026

Copy link
Copy Markdown

🎯 Code Coverage (details)
Patch Coverage: 81.00%
Overall Coverage: 84.66% (-0.13%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 64ac7f1 | Docs | Datadog PR Page | Give us feedback!

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a4b98f13d7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkg/lockfile/swift/parse-package-resolved.go
Comment thread pkg/lockfile/swift/parse-package-resolved.go

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: be6d78e1ba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkg/lockfile/swift/match-package-swift.go Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a5ab3c8f59

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkg/lockfile/swift/match-package-swift.go
Base automatically changed from ander/lockfile-location-role-field to main April 30, 2026 07:47
@anderruiz anderruiz force-pushed the ander/shift-package-manager branch from a5ab3c8 to 8ef14af Compare April 30, 2026 07:55

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1601d3127e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


if !inBlock {
// Look for .package( to start a dependency block
idx := strings.Index(line, ".package(")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Ignore commented-out package declarations

When Package.swift contains a commented-out .package(url: ...) for a package that is still present in Package.resolved (for example because it is transitive, or the lockfile is stale), this unconditional substring search records the comment as a real manifest entry. Match then marks that package IsDirect=true and reports the comment as the manifest location, which can skew direct-dependency metadata and reachability inputs; skip Swift line/block comments before looking for dependency declarations.

Useful? React with 👍 / 👎.


if !inBlock {
// Look for .package( to start a dependency block
idx := strings.Index(line, ".package(")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Handle multiple package calls on one line

When dependencies are formatted compactly on one line, such as dependencies: [.package(...), .package(...)], this only finds the first .package( and then treats the rest of the line as one block; extractEntryFromBlock extracts only the first url: from that block. Any later direct dependencies on the same line are left out of entryMap, so they remain marked as transitive and lose their manifest location.

Useful? React with 👍 / 👎.

@anderruiz anderruiz changed the title Add Swift Package Manager parser for Package.resolved Add Swift Package Manager parser for Package.swift/resolved Apr 30, 2026

@piloulacdog piloulacdog left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

something we might be missing is to know: "how good is our parser compared to competitor"? have we done some analysis about this?

We have the tools to do so. I didn't spend much time on trying to find the right repository to test, but randomly picking for example: https://github.com/muh-nee/sca-testing-Signal-iOS (which has no Packages.resolved - only has Packgaes.swift) and https://github.com/muh-nee/sca-testing-firefox-ios (has both) we can already tests them and see some results here and there and it could guide us on identifying potential gaps / or prove that we are better at detecting libraries.

Comment thread internal/utility/purl/purl.go Outdated
models.EcosystemPub: "pub",
models.EcosystemHex: packageurl.TypeHex,
models.EcosystemCRAN: packageurl.TypeCran,
models.EcosystemSwiftURL: "swift",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we do like the other ecosystem (except pub) and have an actual: packageurl enum?

tests := []struct {
name string
packageName string
wantNamespace string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: but in tests usually this kind of variables are using the keyword "expected" over "want" (which I almost never seen - doesn't mean it doesn't exist - just that's pretty uncommon to me)

Comment thread cmd/datadog-sbom-generator/__snapshots__/main_test.snap
// Build a lookup map from normalized name to entry
entryMap := make(map[string]packageEntry, len(entries))
for _, entry := range entries {
entryMap[entry.name] = entry

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that because GitHub URLs are case-insensitive, so a Package.resolved with https://github.com/alamofire/alamofire.git paired with a Package.swift referencing https://github.com/Alamofire/Alamofire.git (or vice-versa) won't match, and the package stays IsDirect=false

Suggested change
entryMap[entry.name] = entry
entryMap[strings.ToLower(entry.name)] = entry

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we would need to validate it with a test

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But do you think this is a real case given resolved is autogenerated?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

checked locally and:

when in Package.swift

.package(url: "https://github.com/Alamofire/Alamofire.git", from: "5.9.0")
...
.product(name: "Alamofire", package: "Alamofire")

then Package.resolved

    {
      "identity" : "alamofire",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/Alamofire/Alamofire.git",
      "state" : {
        "revision" : "e938f8c66708e7352fc7e3512647fa54255b267a",
        "version" : "5.11.2"
      }
    }

and when:

.package(url: "https://github.com/alamofire/alamofire.git", from: "5.9.0"),
...
.product(name: "Alamofire", package: "alamofire"), # name should match the manifest of the library - cannot be lower case 

then Package.resolved

    {
      "identity" : "alamofire",
      "kind" : "remoteSourceControl",
      "location" : "https://github.com/Alamofire/Alamofire.git",
      "state" : {
        "revision" : "e938f8c66708e7352fc7e3512647fa54255b267a",
        "version" : "5.11.2"
      }
    },


// Enrich matching packages
for i := range packages {
entry, ok := entryMap[packages[i].Name]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above

Suggested change
entry, ok := entryMap[packages[i].Name]
entry, ok := entryMap[strings.ToLower(packages[i].Name)]

// direct-dependency information from Package.swift.
type PackageSwiftMatcher struct{}

var urlRegexp = cachedregexp.MustCompile(`url:\s*"([^"]+)"`)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From reading a bit the specs of swift, it seems that the Swift Package Registry .package(id: ...) is never detected as direct

This regex only matches the URL-based declaration form. When Swift 5.7+ registry deps use .package(id: "scope.name", from: ...) with no url: field, so a registry-kind pin paired with a matching .package(id:) in Package.swift stays IsDirect=false.

A complete fix probably needs two changes:

  1. Here, also extract id:\s*"([^"]+)" so registry-style declarations are picked up.
  2. In parse-package-resolved.go around line 90, key registry pins (pin.Kind == "registry") by pin.Identity instead of nameFromRepoURL(pin.Location), so the matcher key actually lines up with the id: value.

If we want to defer registry support to a follow-up, that's fine: a code comment / TODO here calling out the limitation would be enough so it isn't silently incorrect + explaining the limitation in the README.md (just like we do for other languages where we explain some limitations)

@anderruiz

Copy link
Copy Markdown
Contributor Author

something we might be missing is to know: "how good is our parser compared to competitor"? have we done some analysis about this?

We have the tools to do so. I didn't spend much time on trying to find the right repository to test, but randomly picking for example: https://github.com/muh-nee/sca-testing-Signal-iOS (which has no Packages.resolved - only has Packgaes.swift) and https://github.com/muh-nee/sca-testing-firefox-ios (has both) we can already tests them and see some results here and there and it could guide us on identifying potential gaps / or prove that we are better at detecting libraries.

@piloulacdog I know for sure that Package.resolved is missing in several repos, because I started with that approach for the crawlers and it was not working, but I am assuming that as a reasonable limitation that we can address in a similar way to what we will do in NPM to allow lockless reporting. I would however start with the normal approach of assuming both needs to be present

anderruiz and others added 17 commits April 30, 2026 12:05
Pass LocationRoleManifest to NewPackageLocations in the scanner's
vulnerability result builder. Update test fixture literals and
snapshots to include the new Role field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds SwiftPM PackageManager constant, Swift language, SwiftFilePath
("Package.resolved"), and a pkg:swift purl extractor that derives
namespace/name from the repository URL (strips .git suffix, lowercases
the last two path segments).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ackage.swift matcher

Extractor parses all three Package.resolved formats:
- v1 (Xcode 11): object.pins[] with repositoryURL
- v2 (Xcode 13): pins[] with location
- v3 (Xcode 15.3): same as v2 plus originHash

localSourceControl pins are skipped (no meaningful purl URL).
All packages get LocationRole=lockfile and IsDirect=false by default.

The Package.swift Matcher enriches packages found in the sibling
Package.swift manifest: sets IsDirect=true, LocationRole=manifest,
and BlockLocation pointing to the .package(url:...) declaration line
that the developer actually edits to update a dependency.

Registers via init() into pkg/lockfile/parsers/parsers.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The BlockLocation was using Column{0,0} which failed
IsFilePositionExtractedSuccessfully and caused locations to be silently
dropped. Use GetFirstNonEmptyCharacterIndexInLine / GetLastNonEmpty to
extract real column bounds from the .package( source line, matching the
pattern used by all other matchers (e.g. Cargo.toml).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Registry pins (kind: "registry") have an empty location and store
the package identifier in the identity field. Previously they were
silently dropped because nameFromRepoURL("") returns "".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
git@github.com:org/repo.git style URLs were silently dropped because
url.Parse does not recognise them as having a host. A regex handles
this format before falling back to url.Parse.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A line-comment stripper removes // ... (excluding :// in URLs) before
searching for .package(, so transitive packages that coincidentally
match a commented-out declaration are not incorrectly marked IsDirect.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…wift

Previously only the first .package( per line was detected. The parser
now tracks the closing paren position and continues scanning the
remainder of the line for additional declarations.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GitHub URLs are case-insensitive. If Package.swift and Package.resolved
use different casings for the same repository (e.g. after a manual URL
edit without re-resolving), the lookup now still matches correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a dependency is pinned to a branch instead of a version tag,
State.Version is empty and State.Branch holds the branch name.
Previously these packages were emitted with an empty version;
now the branch name is used, matching Trivy's behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@anderruiz anderruiz force-pushed the ander/shift-package-manager branch from 1601d31 to 1a39688 Compare April 30, 2026 11:53
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use integer range (intrange) in stripLineComment loop
- Add blank lines before break statements (nlreturn)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 65f75ca5df

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +12 to +13
if len(nameParts) < 2 || len(packageInfo.Name) == 0 {
err = fmt.Errorf("invalid swift package_name (%s)", packageInfo.Name)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept registry Swift identities when building PURLs

When a registry pin is extracted, the parser now emits the dotted pin.identity as the package name (for example apple.swift-argument-parser), but sanitizeScannedPackages still sends that name through this splitter, which requires a slash and returns an error. That causes every registry-resolved Swift dependency to be dropped before SBOM/vulnerability output; the fresh evidence beyond the earlier registry-parser comment is that the identity is now preserved by extraction but rejected during PURL conversion.

Useful? React with 👍 / 👎.

Comment on lines +93 to +95
if resolved.Version == 1 {
if isLocalURL(pin.repoURL) {
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not classify v1 scp SSH URLs as local

For v1 Package.resolved files that store a private repository as repositoryURL: "git@github.com:org/repo.git", this v1-only guard calls isLocalURL; net/url.Parse errors on scp-style URLs and isLocalURL treats that as local, so the pin is skipped before the scp handler in nameFromRepoURL can run. The fresh evidence beyond the earlier SSH comment is that the v2 path now handles scp URLs, but this v1 guard still drops the same URL form.

Useful? React with 👍 / 👎.

@anderruiz anderruiz requested a review from piloulacdog May 4, 2026 12:32
piloulacdog
piloulacdog previously approved these changes May 4, 2026
Adds an idRegexp to extract the id: "scope.name" argument from
.package(id: ...) blocks. Registry pins in Package.resolved already
use pin.Identity as the package name, so the two values align and
IsDirect is correctly set to true.

URL resolution from scope.name to a repository URL is left to the backend.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 64ac7f1e66

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +111 to +115
// Use branch as version when there is no version tag (branch-pinned dependency).
version := pin.version
if version == "" {
version = pin.branch
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the resolved revision for unversioned pins

When a Swift dependency is pinned by branch or exact revision, state.version is empty and the lockfile's only exact resolved identifier is state.revision; this fallback stores the branch name (or leaves Version empty for .revision(...)) even though Commit is populated. sanitizeScannedPackages then creates a Swift PURL from only name/version, and groupBySource takes the ecosystem/name path before the commit-only fallback, so vulnerability output for those pins carries pkg:swift/...@main or no version instead of the resolved commit. Please use the revision as the version for unversioned pins, or propagate Commit into PackageInfo for Swift packages.

Useful? React with 👍 / 👎.

@anderruiz anderruiz requested a review from piloulacdog May 5, 2026 14:24
@anderruiz anderruiz merged commit 5ec4241 into main May 6, 2026
11 checks passed
@anderruiz anderruiz deleted the ander/shift-package-manager branch May 6, 2026 16:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants