Skip to content

fix(dotnet): identify deps.json root project from dependency graph#10954

Open
jetersen wants to merge 6 commits into
aquasecurity:mainfrom
jetersen:fix/dotnet-deps-deterministic-root
Open

fix(dotnet): identify deps.json root project from dependency graph#10954
jetersen wants to merge 6 commits into
aquasecurity:mainfrom
jetersen:fix/dotnet-deps-deterministic-root

Conversation

@jetersen

@jetersen jetersen commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Description

A .deps.json can legitimately contain multiple type: project libraries: the application itself and each <ProjectReference> built from source. The parser previously treated the first project library as the root and dropped every other project library. Because Go map iteration is unordered, this could select a different root between scans, omit first-party dependencies from the inventory, and produce an incorrect dependency graph.

This PR now:

  • identifies the root project from the selected targets graph as the project that no other target depends on;
  • retains the remaining project libraries as normal direct or indirect dependencies; and
  • falls back to the first lexicographically sorted project when the graph does not identify exactly one root.

For a published application with the graph Web -> Api -> Data, plus a direct Newtonsoft.Json dependency from Web, the parser now reports:

  • Web as root;
  • Api and Newtonsoft.Json as direct dependencies; and
  • Data as an indirect dependency.

The regression test exercises Parse end to end using a real .deps.json produced by dotnet publish. The minimal project sources and regeneration instructions are retained with the fixture.

Related issues

Checklist

  • I've read the guidelines for contributing to this repository.
  • I've followed the conventions in the PR title.
  • I've added tests that prove my fix is effective or that my feature works.
  • I've updated the documentation with the relevant information (if needed).
  • I've added usage information (if the PR introduces new options)
  • I've included a "before" and "after" example to the description (if the PR is a user interface change).

collectPackages ranged over the libraries map and kept the first
library with type "project" as the root, so Go map iteration order
decided which assembly was reported when multiple project libraries
exist. Iterate sorted library keys so the selection is stable across
runs.
@jetersen jetersen force-pushed the fix/dotnet-deps-deterministic-root branch from 1cc5415 to c6cb3e1 Compare July 11, 2026 16:37

@DmitriyLewen DmitriyLewen 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.

Hello @jetersen
Thanks for digging into this — the non-determinism you describe is real, and making the output reproducible is a goal worth having.
Before we merge, though, I'd like us to fix the underlying cause rather than only the symptom, because sorting the keys makes the choice stable but not correct.

The key thing to realize is that type: project does not mean "root".
A .deps.json legitimately contains several type: project libraries: the application itself plus every <ProjectReference> that is built from source in the same solution.
So "multiple project libraries" is a normal, common case for any multi-project solution — not a corner case — and the existing Warn("Multiple root projects found") is itself misleading.
type: project really means "built from source here" (as opposed to type: package, i.e. pulled from NuGet), and exactly one of those projects is the real application entry point.

That is why picking the lexicographically first project isn't enough.
It is deterministic, but the first project alphabetically has no relationship to which one is the entry point, so we can now stably mark the wrong library as root.
On top of that, the current continue silently drops every other type: project library from the inventory — but those are first-party assemblies that are ordinary direct/indirect dependencies of the app, and they should stay in the SBOM.

The direction I'd suggest: the real root is the type: project library that nothing else depends on in the targets graph (the top of the graph), and the other projects should stay in the inventory as ordinary dependencies rather than being dropped.
A deterministic fallback (e.g. the first project) still makes sense when there's no graph to look at.
The exact implementation is up to you.

As a test case, a small multi-project fixture makes the difference obvious.

{
  "runtimeTarget": {
    "name": ".NETCoreApp,Version=v8.0",
    "signature": ""
  },
  "compilationOptions": {},
  "targets": {
    ".NETCoreApp,Version=v8.0": {
      "Web/1.0.0": {
        "dependencies": {
          "Api": "1.0.0",
          "Newtonsoft.Json": "13.0.3"
        },
        "runtime": {
          "Web.dll": {}
        }
      },
      "Api/1.0.0": {
        "dependencies": {
          "Data": "1.0.0"
        },
        "runtime": {
          "Api.dll": {}
        }
      },
      "Data/1.0.0": {
        "runtime": {
          "Data.dll": {}
        }
      },
      "Newtonsoft.Json/13.0.3": {
        "runtime": {
          "lib/netstandard2.0/Newtonsoft.Json.dll": {
            "assemblyVersion": "13.0.0.0",
            "fileVersion": "13.0.3.27908"
          }
        }
      }
    }
  },
  "libraries": {
    "Web/1.0.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    },
    "Api/1.0.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    },
    "Data/1.0.0": {
      "type": "project",
      "serviceable": false,
      "sha512": ""
    },
    "Newtonsoft.Json/13.0.3": {
      "type": "package",
      "serviceable": true,
      "sha512": "sha512-HrC5BXdl00IP9zeV+0Z848QWPAoCr9P3bDEZguI+gkLcBKAOxix/tLEAAHC+UvDNPv4a2d18lOReHMOagPa+zQ==",
      "path": "newtonsoft.json/13.0.3",
      "hashPath": "newtonsoft.json.13.0.3.nupkg.sha512"
    }
  }
}

Consider a solution Web → Api → Data (plus a NuGet package), where all three of Web, Api, Data are type: project:

  • with sorted keys, Api wins (first alphabetically) and Web/Data are dropped;
  • with the graph rule, Web is correctly the root (nothing depends on it), Api becomes direct, Data indirect, and nothing is dropped.

But it's synthetic, though — it would be better to build a real minimal example (a tiny multi-project solution, dotnet publish, take its .deps.json) and use that in the tests, asserting the result through Parse (end to end), not only through collectPackages.

@jetersen

Copy link
Copy Markdown
Contributor Author

Ya, I was considering that approach but went with minimal implementation first.
I'll setup a sample and publish.

@jetersen

Copy link
Copy Markdown
Contributor Author

@DmitriyLewen let me know if this is what you were looking for or should I remove the sample csproj or the .gitattributes changes

@jetersen jetersen changed the title fix(dotnet): select deps.json root project deterministically fix(dotnet): identify deps.json root project from dependency graph Jul 13, 2026
Comment thread pkg/dependency/parser/dotnet/core_deps/parse.go Outdated
Comment thread pkg/dependency/parser/dotnet/core_deps/parse.go Outdated
Comment thread pkg/dependency/parser/dotnet/core_deps/parse.go Outdated
Comment thread pkg/dependency/parser/dotnet/core_deps/parse.go Outdated
Comment thread pkg/dependency/parser/dotnet/core_deps/parse.go Outdated
Comment thread pkg/dependency/parser/dotnet/core_deps/parse_test.go
@jetersen

Copy link
Copy Markdown
Contributor Author
  • Renamed projectNameVer to rootPkgID.
  • Project relationships are assigned only when a unique root exists.
  • Ambiguous roots now leave every package as RelationshipUnknown.
  • Consolidated multi-project and ambiguous-root coverage into pkg/dependency/parser/dotnet/core_deps/parse_test.go:220.
  • Added pkg/dependency/parser/dotnet/core_deps/testdata/ambiguous-root.deps.json.
  • Removed the separate new test functions.

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.

dotnet: incorrect and non-deterministic root project selection for .deps.json with multiple type: project libraries

2 participants