Target repository: https://github.com/oss-review-toolkit/ort
Component: Spdx package manager plugin (plugins/package-managers/spdx)
ORT version: 83.0.1
Summary
The Spdx analyzer plugin (SPDX 3.0.1) does not use dependsOn or contains
relationships when building its dependency graph. Every package in the document
is emitted as a flat, direct dependency of the project regardless of its actual
position in the dependency graph. Tools and reports that rely on direct vs.
transitive classification receive no useful depth information.
Environment
| Item |
Value |
| ORT version |
83.0.1 |
| ORT image |
ghcr.io/oss-review-toolkit/ort:83.0.1 |
| Plugin enabled via |
.ort.yml with enabled_package_managers: [Spdx] |
Steps to reproduce
Save the following minimal SPDX 3.0.1 file as example.spdx.json:
{
"@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld",
"@graph": [
{
"type": "CreationInfo",
"@id": "_:ci",
"specVersion": "3.0.1",
"created": "2026-01-01T00:00:00Z",
"createdBy": ["https://example.org/dep-tree-example/agent/ExampleOrg"],
"createdUsing": ["https://example.org/dep-tree-example/tool/example-tool"]
},
{
"type": "Organization",
"spdxId": "https://example.org/dep-tree-example/agent/ExampleOrg",
"creationInfo": "_:ci",
"name": "Example Organization"
},
{
"type": "Tool",
"spdxId": "https://example.org/dep-tree-example/tool/example-tool",
"creationInfo": "_:ci",
"name": "example-tool"
},
{
"type": "SpdxDocument",
"spdxId": "https://example.org/dep-tree-example/document/example",
"creationInfo": "_:ci",
"name": "dep-tree-example",
"rootElement": ["https://example.org/dep-tree-example/sbom/example-image"],
"profileConformance": ["core", "software"]
},
{
"type": "software_Sbom",
"spdxId": "https://example.org/dep-tree-example/sbom/example-image",
"creationInfo": "_:ci",
"name": "example-image",
"software_sbomType": ["build"],
"rootElement": ["https://example.org/dep-tree-example/package/example-image"]
},
{
"type": "software_Package",
"spdxId": "https://example.org/dep-tree-example/package/example-image",
"creationInfo": "_:ci",
"name": "example-image",
"software_packageVersion": "1.0",
"software_primaryPurpose": "install"
},
{
"type": "software_Package",
"spdxId": "https://example.org/dep-tree-example/package/app-a",
"creationInfo": "_:ci",
"name": "app-a",
"software_packageVersion": "2.0",
"software_primaryPurpose": "install"
},
{
"type": "software_Package",
"spdxId": "https://example.org/dep-tree-example/package/app-standalone",
"creationInfo": "_:ci",
"name": "app-standalone",
"software_packageVersion": "1.5",
"software_primaryPurpose": "install"
},
{
"type": "software_Package",
"spdxId": "https://example.org/dep-tree-example/package/lib-b",
"creationInfo": "_:ci",
"name": "lib-b",
"software_packageVersion": "3.1",
"software_primaryPurpose": "install"
},
{
"type": "software_Package",
"spdxId": "https://example.org/dep-tree-example/package/lib-c",
"creationInfo": "_:ci",
"name": "lib-c",
"software_packageVersion": "1.0",
"software_primaryPurpose": "install"
},
{
"type": "software_Package",
"spdxId": "https://example.org/dep-tree-example/package/lib-d",
"creationInfo": "_:ci",
"name": "lib-d",
"software_packageVersion": "2.2",
"software_primaryPurpose": "install"
},
{
"type": "Relationship",
"spdxId": "https://example.org/dep-tree-example/relationship/sbom-contains",
"creationInfo": "_:ci",
"from": "https://example.org/dep-tree-example/sbom/example-image",
"relationshipType": "contains",
"to": [
"https://example.org/dep-tree-example/package/app-a",
"https://example.org/dep-tree-example/package/app-standalone"
]
},
{
"type": "Relationship",
"spdxId": "https://example.org/dep-tree-example/relationship/app-a-deps",
"creationInfo": "_:ci",
"from": "https://example.org/dep-tree-example/package/app-a",
"relationshipType": "dependsOn",
"to": [
"https://example.org/dep-tree-example/package/lib-b",
"https://example.org/dep-tree-example/package/lib-c"
]
},
{
"type": "Relationship",
"spdxId": "https://example.org/dep-tree-example/relationship/lib-b-deps",
"creationInfo": "_:ci",
"from": "https://example.org/dep-tree-example/package/lib-b",
"relationshipType": "dependsOn",
"to": [
"https://example.org/dep-tree-example/package/lib-d"
]
}
]
}
The intended dependency graph is:
SBOM
├── app-a (direct) ← listed in SBOM's contains
│ ├── lib-b (transitive) ← app-a dependsOn lib-b
│ │ └── lib-d (transitive, depth 2) ← lib-b dependsOn lib-d
│ └── lib-c (transitive) ← app-a dependsOn lib-c
└── app-standalone (direct) ← listed in SBOM's contains
Add a .ort.yml next to the file:
analyzer:
enabled_package_managers:
- Spdx
Run analysis:
ort analyze -i /path/to/dir -o /path/to/dir/ort-out
Actual behaviour
ORT reports all 6 packages as direct dependencies with no tree structure:
Found 1 project(s) and 6 package(s) in total.
Resolved issues: 0 errors, 0 warnings, 0 hints.
Unresolved issues: 0 errors, 0 warnings, 0 hints.
The dependency_graphs section of the analyzer result YAML:
dependency_graphs:
SPDX:
packages:
- SPDX::app-a:2.0
- SPDX::app-standalone:1.5
- SPDX::example-image:1.0
- SPDX::lib-b:3.1
- SPDX::lib-c:1.0
- SPDX::lib-d:2.2
nodes:
- {pkg: 1} # no "dependencies" key on any node
- {pkg: 2}
- {pkg: 3}
- {pkg: 4}
- {pkg: 5}
- {}
scopes:
':dep-tree-example::install':
- {root: 0} # all 6 packages listed as direct scope roots
- {root: 1}
- {root: 2}
- {root: 3}
- {root: 4}
- {root: 5}
Expected behaviour
Only the two packages referenced in the SBOM's contains relationship should
be direct scope entries. Packages reachable exclusively via dependsOn chains
should be transitive, with dependencies populated on their parent nodes:
dependency_graphs:
SPDX:
packages:
- SPDX::app-a:2.0
- SPDX::app-standalone:1.5
- SPDX::example-image:1.0
- SPDX::lib-b:3.1
- SPDX::lib-c:1.0
- SPDX::lib-d:2.2
nodes:
- pkg: 0 # app-a
dependencies: [2, 3] # lib-b, lib-c
- pkg: 1 # app-standalone (leaf)
- pkg: 3 # lib-b
dependencies: [4] # lib-d
- pkg: 4 # lib-c (leaf)
- pkg: 5 # lib-d (leaf)
scopes:
':dep-tree-example::install':
- {root: 0} # app-a (direct via contains)
- {root: 1} # app-standalone (direct via contains)
# lib-b, lib-c, lib-d are transitive — not listed as scope roots
Root cause
In plugins/package-managers/spdx/src/main/kotlin/Spdx.kt, the
resolveDependencies method groups all SpdxPackage elements by
primaryPurpose and creates a flat PackageReference for each one:
// Spdx.kt:110–125
packagesByScope.forEach { (scopeName, packages) ->
val packageRefs = packages.mapNotNullTo(mutableSetOf()) { spdxPkg ->
val ortPackage = spdxPkg.toOrtPackage(licenseMap) ?: return@mapNotNullTo null
ortPackages.merge(...)
PackageReference(ortPackage.id) // ← no `dependencies` populated
}
if (packageRefs.isNotEmpty()) {
scopes += Scope(name = scopeName, dependencies = packageRefs)
}
}
All relationships are retrieved at line 94 but the only method that consumes
them, buildLicenseMap() (line 161), filters exclusively for
HAS_DECLARED_LICENSE and HAS_CONCLUDED_LICENSE. The dependsOn and
contains relationship types are silently ignored for the purpose of
dependency graph construction.
Suggested fix
Add a dependency-tree building step after packages are collected:
- Find the
software_Sbom element's contains relationships to identify the
direct (scope-root) package set.
- Build an adjacency map from
dependsOn relationships:
Map<String, Set<String>> keyed by from element URI.
- Recursively construct
PackageReference nodes with populated dependencies
starting from the direct set, using a visited set to handle shared
dependencies (a package can be depended on by multiple parents).
- Use only the direct set as scope root entries rather than all packages.
This is the same recursive PackageReference-tree pattern used by most other
ORT package manager plugins (e.g. Maven, Npm).
Additional context
Target repository: https://github.com/oss-review-toolkit/ort
Component:
Spdxpackage manager plugin (plugins/package-managers/spdx)ORT version: 83.0.1
Summary
The
Spdxanalyzer plugin (SPDX 3.0.1) does not usedependsOnorcontainsrelationships when building its dependency graph. Every package in the document
is emitted as a flat, direct dependency of the project regardless of its actual
position in the dependency graph. Tools and reports that rely on direct vs.
transitive classification receive no useful depth information.
Environment
ghcr.io/oss-review-toolkit/ort:83.0.1.ort.ymlwithenabled_package_managers: [Spdx]Steps to reproduce
Save the following minimal SPDX 3.0.1 file as
example.spdx.json:{ "@context": "https://spdx.org/rdf/3.0.1/spdx-context.jsonld", "@graph": [ { "type": "CreationInfo", "@id": "_:ci", "specVersion": "3.0.1", "created": "2026-01-01T00:00:00Z", "createdBy": ["https://example.org/dep-tree-example/agent/ExampleOrg"], "createdUsing": ["https://example.org/dep-tree-example/tool/example-tool"] }, { "type": "Organization", "spdxId": "https://example.org/dep-tree-example/agent/ExampleOrg", "creationInfo": "_:ci", "name": "Example Organization" }, { "type": "Tool", "spdxId": "https://example.org/dep-tree-example/tool/example-tool", "creationInfo": "_:ci", "name": "example-tool" }, { "type": "SpdxDocument", "spdxId": "https://example.org/dep-tree-example/document/example", "creationInfo": "_:ci", "name": "dep-tree-example", "rootElement": ["https://example.org/dep-tree-example/sbom/example-image"], "profileConformance": ["core", "software"] }, { "type": "software_Sbom", "spdxId": "https://example.org/dep-tree-example/sbom/example-image", "creationInfo": "_:ci", "name": "example-image", "software_sbomType": ["build"], "rootElement": ["https://example.org/dep-tree-example/package/example-image"] }, { "type": "software_Package", "spdxId": "https://example.org/dep-tree-example/package/example-image", "creationInfo": "_:ci", "name": "example-image", "software_packageVersion": "1.0", "software_primaryPurpose": "install" }, { "type": "software_Package", "spdxId": "https://example.org/dep-tree-example/package/app-a", "creationInfo": "_:ci", "name": "app-a", "software_packageVersion": "2.0", "software_primaryPurpose": "install" }, { "type": "software_Package", "spdxId": "https://example.org/dep-tree-example/package/app-standalone", "creationInfo": "_:ci", "name": "app-standalone", "software_packageVersion": "1.5", "software_primaryPurpose": "install" }, { "type": "software_Package", "spdxId": "https://example.org/dep-tree-example/package/lib-b", "creationInfo": "_:ci", "name": "lib-b", "software_packageVersion": "3.1", "software_primaryPurpose": "install" }, { "type": "software_Package", "spdxId": "https://example.org/dep-tree-example/package/lib-c", "creationInfo": "_:ci", "name": "lib-c", "software_packageVersion": "1.0", "software_primaryPurpose": "install" }, { "type": "software_Package", "spdxId": "https://example.org/dep-tree-example/package/lib-d", "creationInfo": "_:ci", "name": "lib-d", "software_packageVersion": "2.2", "software_primaryPurpose": "install" }, { "type": "Relationship", "spdxId": "https://example.org/dep-tree-example/relationship/sbom-contains", "creationInfo": "_:ci", "from": "https://example.org/dep-tree-example/sbom/example-image", "relationshipType": "contains", "to": [ "https://example.org/dep-tree-example/package/app-a", "https://example.org/dep-tree-example/package/app-standalone" ] }, { "type": "Relationship", "spdxId": "https://example.org/dep-tree-example/relationship/app-a-deps", "creationInfo": "_:ci", "from": "https://example.org/dep-tree-example/package/app-a", "relationshipType": "dependsOn", "to": [ "https://example.org/dep-tree-example/package/lib-b", "https://example.org/dep-tree-example/package/lib-c" ] }, { "type": "Relationship", "spdxId": "https://example.org/dep-tree-example/relationship/lib-b-deps", "creationInfo": "_:ci", "from": "https://example.org/dep-tree-example/package/lib-b", "relationshipType": "dependsOn", "to": [ "https://example.org/dep-tree-example/package/lib-d" ] } ] }The intended dependency graph is:
Add a
.ort.ymlnext to the file:Run analysis:
Actual behaviour
ORT reports all 6 packages as direct dependencies with no tree structure:
The
dependency_graphssection of the analyzer result YAML:Expected behaviour
Only the two packages referenced in the SBOM's
containsrelationship shouldbe direct scope entries. Packages reachable exclusively via
dependsOnchainsshould be transitive, with
dependenciespopulated on their parent nodes:Root cause
In
plugins/package-managers/spdx/src/main/kotlin/Spdx.kt, theresolveDependenciesmethod groups allSpdxPackageelements byprimaryPurposeand creates a flatPackageReferencefor each one:All relationships are retrieved at line 94 but the only method that consumes
them,
buildLicenseMap()(line 161), filters exclusively forHAS_DECLARED_LICENSEandHAS_CONCLUDED_LICENSE. ThedependsOnandcontainsrelationship types are silently ignored for the purpose ofdependency graph construction.
Suggested fix
Add a dependency-tree building step after packages are collected:
software_Sbomelement'scontainsrelationships to identify thedirect (scope-root) package set.
dependsOnrelationships:Map<String, Set<String>>keyed byfromelement URI.PackageReferencenodes with populateddependenciesstarting from the direct set, using a visited set to handle shared
dependencies (a package can be depended on by multiple parents).
This is the same recursive
PackageReference-tree pattern used by most otherORT package manager plugins (e.g.
Maven,Npm).Additional context
Linux builds, where
dependsOnrelationships encode runtime packagedependencies. A real-world image of ~1400 packages has 268 genuine direct
dependencies and 1138 transitive ones; ORT currently reports all 1407 as
direct.
IllegalArgumentExceptionwhen input directory is inside a Git repository #11640 was found during the sameinvestigation.