Skip to content

fix(cargo): resolve workspace membership by identity, not name alone - #407

Open
bomly-guy wants to merge 1 commit into
mainfrom
claude/github-issue-399-bf4d98
Open

fix(cargo): resolve workspace membership by identity, not name alone#407
bomly-guy wants to merge 1 commit into
mainfrom
claude/github-issue-399-bf4d98

Conversation

@bomly-guy

Copy link
Copy Markdown
Collaborator

Fixes #399.

What was wrong

depGraphFromLockWorkspace resolved workspace membership and lock packages through name-keyed maps, so when a workspace member and an unrelated crate shared a name, the last same-named lock record won: the member node was emitted under the external crate's version, carried its ResolvedURL, and the external crate was absent from the graph entirely — first-party code misreported under someone else's identity, and a real dependency lost to vulnerability matching. The single-package lock path had the same collision against the root package's name, and dependency references were truncated to bare crate names, resolving every reference to whichever same-named record came first in the file.

The fix

  • One home for the membership rule (isProjectLockRecord / projectLockRecord in lock_index.go): a project's own lock record matches by name, declared version, and the absence of a source — a workspace member's record never carries one. Workspace version inheritance may leave the member manifest without a version; the source-less name match then stands alone. Both lock paths (workspace partitioning and the single-package root) route through it, and only the claimed records are withheld from the ordinary dependency pass, so a same-named external crate keeps its own node, origin, and edges.
  • Full-precision dependency references: parseCargoLockPackages keeps the whole reference string ("name", "name version", "name version (source)") instead of the first field. Cargo.lock qualifies a reference exactly when a bare name would be ambiguous, so version-qualified refs now resolve to the exact record (see TestCargoLockVersionQualifiedDependencyRefsResolveExactly — previously both helper 1.0.0 and helper 2.0.0 refs resolved to whichever record came first). Direct manifest edges resolve through each application root's own lock record refs at the same precision.
  • Metadata path: cargo's source-qualified package IDs already keep colliding records distinct (since feat(sbom): detector-asserted package origin in SBOM export #397's EnsureOccurrence); workspace members now insert first, so on an exact name@version collision the project's own package keeps the plain node ID and the external record becomes the qualified occurrence — not the other way around by accident of sort order.

Coverage

New identity_test.go reproduces the issue's exact scenario (member helper 0.1.0 + external helper 1.0.0 from a git remote) on the lock workspace path, plus: workspace version inheritance, the metadata path (different-version and exact name@version collisions), the single-package root collision, and version-qualified reference resolution. TestCargoWorkspaceMemberTakesNoExternalOrigin's "not fixed here" caveat is updated to point at the new tests.

Goldens

scan-cargo and scan-cargo-workspace smoke cases pass unchanged locally — the pinned fixtures hold no colliding names and no version-ambiguous references, so no graph shape moves there. make test, the cargo fuzz target (FuzzDepGraphFromCargoLock, 10s), gofmt, and go vet are clean.

🤖 Generated with Claude Code

A workspace member and an unrelated crate can share a name. The lock
workspace path resolved membership through name-keyed maps, so the last
same-named lock record won: the member was emitted under the external
crate's version with its ResolvedURL, and the external crate vanished
from the graph entirely — misstating first-party code and dropping a
real dependency from vulnerability matching.

Fixes #399:

- isProjectLockRecord/projectLockRecord centralize the membership rule:
  a project's own lock record matches by name, declared version, and
  the absence of a source (workspace version inheritance may leave the
  manifest without a version, in which case the source-less name match
  stands alone). Both lock paths — workspace partitioning and the
  single-package root — route through it, and only the claimed records
  are withheld from the ordinary dependency pass, so a same-named
  external crate keeps its own node.
- parseCargoLockPackages keeps full dependency reference strings
  instead of truncating to the crate name; Cargo.lock qualifies a
  reference with version (and source) exactly when a bare name would be
  ambiguous, and truncation resolved every reference to whichever
  same-named record came first. Direct manifest edges resolve through
  the root's own lock record refs at the same precision.
- The cargo metadata path inserts workspace members before other
  records, so on an exact name@version collision the project's own
  package keeps the plain node ID and the external record becomes the
  qualified occurrence, not the other way around by sort order.

Regression coverage in identity_test.go spans both lock paths and the
metadata path; scan-cargo and scan-cargo-workspace smoke goldens are
unchanged (the pinned fixtures have no colliding or version-ambiguous
records).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 26 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8cfdbb86-faec-4baa-8246-cc17fab82fd2

📥 Commits

Reviewing files that changed from the base of the PR and between 22e3ded and a1c98db.

📒 Files selected for processing (5)
  • internal/detectors/cargo/detector.go
  • internal/detectors/cargo/identity_test.go
  • internal/detectors/cargo/lock_index.go
  • internal/detectors/cargo/origin_test.go
  • internal/detectors/cargo/workspace.go

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

Bomly Diff Summary

Compared 22e3dedb67ada74eca47974be74928c232af7ddb to a1c98db37f5a41d4fdefb3a5c0d894b7d21de0e2.

Overview

Status Manifests Dependencies Findings Duration
✅ Pass +0 / ~0 / -0 0 added / 0 version changed / 0 detail changes / 0 removed 0 introduced / 0 persisted / 0 resolved 1m 24s

Dependency Changes

✅ No dependency changes.

Vulnerabilities

✅ No vulnerability changes.

License Changes

✅ No license changes.

Project Posture

✅ No project posture changes (--matchers +scorecard was not selected).

Policy Findings

✅ No policy differences were identified.

@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: a1c98db37f

ℹ️ 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 strings.TrimSpace(pkg.Source) != "" {
return false
}
return manifest.Version == "" || pkg.Version == manifest.Version

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 Resolve inherited member versions before claiming lock records

In the lockfile workspace fallback, a member using version.workspace = true is parsed with an empty manifest.Version; if the lockfile also contains a same-named source-less path dependency at another version, this predicate accepts both records and projectLockRecord claims whichever appears first. For example, a workspace helper inheriting version 1.0.0 alongside a path dependency helper 0.1.0 causes the latter to become the application root while the real member is treated as a dependency, corrupting identity and vulnerability matching whenever cargo metadata is unavailable or fails. Resolve the inherited workspace version before matching, or reject ambiguous source-less candidates rather than treating an empty version as a wildcard.

Useful? React with 👍 / 👎.

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.

cargo: workspace membership is resolved by package name alone

1 participant