Skip to content

Commit e570866

Browse files
committed
feat: include path dependencies in SBOM with repository_url=local qualifier
1 parent ef2ab37 commit e570866

7 files changed

Lines changed: 90 additions & 74 deletions

File tree

src/providers/rust_cargo.js

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ function handleSingleCrate(metadata, ignoredDeps, includeTransitive, opts, licen
275275
}
276276

277277
if (includeTransitive) {
278-
addTransitiveDeps(sbom, metadata, rootPackageId, ignoredDeps, new Set())
278+
addTransitiveDeps(sbom, metadata, rootPackageId, ignoredDeps, new Set(), rootPurl)
279279
} else {
280280
addDirectDeps(sbom, metadata, rootPackageId, rootPurl, ignoredDeps)
281281
}
@@ -322,9 +322,12 @@ function handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransiti
322322
let memberPackage = findPackageById(metadata, memberId)
323323
if (!memberPackage) {continue}
324324

325-
let memberPurl = toPurl(memberPackage.name, memberPackage.version)
325+
let memberPurl = memberPackage.source == null
326+
? toPathDepPurl(memberPackage.name, memberPackage.version)
327+
: toPurl(memberPackage.name, memberPackage.version)
328+
326329
sbom.addDependency(rootPurl, memberPurl)
327-
addTransitiveDeps(sbom, metadata, memberId, ignoredDeps, new Set())
330+
addTransitiveDeps(sbom, metadata, memberId, ignoredDeps, new Set(), memberPurl)
328331
}
329332
} else {
330333
// Component analysis: only [workspace.dependencies] from root Cargo.toml
@@ -333,14 +336,17 @@ function handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransiti
333336
for (let depName of workspaceDeps) {
334337
if (isDepIgnored(depName, ignoredDeps)) {continue}
335338

336-
let pkg = metadata.packages.find(p => p.name === depName && p.source != null)
339+
let pkg = metadata.packages.find(p => p.name === depName)
337340
if (!pkg) {
338341
let altName = depName.replace(/-/g, '_')
339-
pkg = metadata.packages.find(p => p.name === altName && p.source != null)
342+
pkg = metadata.packages.find(p => p.name === altName)
340343
}
341344
if (!pkg) {continue}
342345

343-
let depPurl = toPurl(pkg.name, pkg.version)
346+
let depPurl = pkg.source == null
347+
? toPathDepPurl(pkg.name, pkg.version)
348+
: toPurl(pkg.name, pkg.version)
349+
344350
sbom.addDependency(rootPurl, depPurl)
345351
}
346352
}
@@ -350,19 +356,19 @@ function handleVirtualWorkspace(manifest, metadata, ignoredDeps, includeTransiti
350356

351357
/**
352358
* Recursively adds transitive dependencies to the SBOM.
353-
* Path dependencies (source == null) are not added to the SBOM, but their
354-
* subtrees are still walked. Any registry dependencies found under a path
355-
* dep are attached to the nearest non-path ancestor via {@link effectiveParentPurl}.
359+
* Path dependencies (source == null) are included with a
360+
* {@code repository_url=local} qualifier so the backend can skip
361+
* vulnerability checks while still showing them in the dependency tree.
356362
* @param {Sbom} sbom - the SBOM to add dependencies to
357363
* @param {object} metadata - parsed cargo metadata
358364
* @param {string} packageId - the package ID to resolve dependencies for
359365
* @param {Set<string>} ignoredDeps - set of ignored dependency names
360366
* @param {Set<string>} visited - set of already-visited package IDs to prevent cycles
361-
* @param {PackageURL} [effectiveParentPurl] - when walking through a path dep,
362-
* the purl of the nearest registry ancestor to attach discovered deps to
367+
* @param {PackageURL} [startingPurl] - purl to use for the starting package,
368+
* so callers can ensure it matches the purl already added to the SBOM
363369
* @private
364370
*/
365-
function addTransitiveDeps(sbom, metadata, packageId, ignoredDeps, visited, effectiveParentPurl) {
371+
function addTransitiveDeps(sbom, metadata, packageId, ignoredDeps, visited, startingPurl) {
366372
if (visited.has(packageId)) {return}
367373
visited.add(packageId)
368374

@@ -372,11 +378,9 @@ function addTransitiveDeps(sbom, metadata, packageId, ignoredDeps, visited, effe
372378
let sourcePackage = findPackageById(metadata, packageId)
373379
if (!sourcePackage) {return}
374380

375-
// For path deps use the effective parent purl so their children attach
376-
// to the nearest real (registry) ancestor in the SBOM.
377-
let sourcePurl = (sourcePackage.source == null && effectiveParentPurl)
378-
? effectiveParentPurl
379-
: toPurl(sourcePackage.name, sourcePackage.version)
381+
let sourcePurl = startingPurl || (sourcePackage.source == null
382+
? toPathDepPurl(sourcePackage.name, sourcePackage.version)
383+
: toPurl(sourcePackage.name, sourcePackage.version))
380384

381385
let runtimeDeps = filterRuntimeDeps(resolveNode)
382386

@@ -385,21 +389,18 @@ function addTransitiveDeps(sbom, metadata, packageId, ignoredDeps, visited, effe
385389
if (!depPackage) {continue}
386390
if (isDepIgnored(depPackage.name, ignoredDeps)) {continue}
387391

388-
if (depPackage.source == null) {
389-
// Path dependency — don't add to SBOM, but walk its deps
390-
// passing our sourcePurl so its children attach correctly
391-
addTransitiveDeps(sbom, metadata, depId, ignoredDeps, visited, sourcePurl)
392-
continue
393-
}
392+
let depPurl = depPackage.source == null
393+
? toPathDepPurl(depPackage.name, depPackage.version)
394+
: toPurl(depPackage.name, depPackage.version)
394395

395-
let depPurl = toPurl(depPackage.name, depPackage.version)
396396
sbom.addDependency(sourcePurl, depPurl)
397397
addTransitiveDeps(sbom, metadata, depId, ignoredDeps, visited)
398398
}
399399
}
400400

401401
/**
402402
* Adds only direct (non-transitive) dependencies to the SBOM.
403+
* Path dependencies are included with a {@code repository_url=local} qualifier.
403404
* @param {Sbom} sbom - the SBOM to add dependencies to
404405
* @param {object} metadata - parsed cargo metadata
405406
* @param {string} packageId - the package ID to resolve dependencies for
@@ -417,9 +418,11 @@ function addDirectDeps(sbom, metadata, packageId, parentPurl, ignoredDeps) {
417418
let depPackage = findPackageById(metadata, depId)
418419
if (!depPackage) {continue}
419420
if (isDepIgnored(depPackage.name, ignoredDeps)) {continue}
420-
if (depPackage.source == null) {continue}
421421

422-
let depPurl = toPurl(depPackage.name, depPackage.version)
422+
let depPurl = depPackage.source == null
423+
? toPathDepPurl(depPackage.name, depPackage.version)
424+
: toPurl(depPackage.name, depPackage.version)
425+
423426
sbom.addDependency(parentPurl, depPurl)
424427
}
425428
}
@@ -626,3 +629,16 @@ function isInDependencySection(section) {
626629
function toPurl(name, version) {
627630
return new PackageURL(ecosystem, undefined, name, version, undefined, undefined)
628631
}
632+
633+
/**
634+
* Creates a PackageURL for a local path dependency, marked with a
635+
* {@code repository_url=local} qualifier so the backend can distinguish
636+
* it from registry packages and skip vulnerability checks.
637+
* @param {string} name - the crate name
638+
* @param {string} version - the crate version
639+
* @returns {PackageURL} the package URL with local qualifier
640+
* @private
641+
*/
642+
function toPathDepPurl(name, version) {
643+
return new PackageURL(ecosystem, undefined, name, version, { repository_url: 'local' }, undefined)
644+
}

test/providers/tst_manifests/cargo/cargo_virtual_workspace/expected_sbom_stack_analysis.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@
2323
{
2424
"name": "crate-a",
2525
"version": "0.1.0",
26-
"purl": "pkg:cargo/crate-a@0.1.0",
26+
"purl": "pkg:cargo/crate-a@0.1.0?repository_url=local",
2727
"type": "library",
28-
"bom-ref": "pkg:cargo/crate-a@0.1.0"
28+
"bom-ref": "pkg:cargo/crate-a@0.1.0?repository_url=local"
2929
},
3030
{
3131
"name": "serde",
@@ -37,9 +37,9 @@
3737
{
3838
"name": "crate-b",
3939
"version": "0.2.0",
40-
"purl": "pkg:cargo/crate-b@0.2.0",
40+
"purl": "pkg:cargo/crate-b@0.2.0?repository_url=local",
4141
"type": "library",
42-
"bom-ref": "pkg:cargo/crate-b@0.2.0"
42+
"bom-ref": "pkg:cargo/crate-b@0.2.0?repository_url=local"
4343
},
4444
{
4545
"name": "tokio",
@@ -60,12 +60,12 @@
6060
{
6161
"ref": "pkg:cargo/cargo_virtual_workspace@0.0.0",
6262
"dependsOn": [
63-
"pkg:cargo/crate-a@0.1.0",
64-
"pkg:cargo/crate-b@0.2.0"
63+
"pkg:cargo/crate-a@0.1.0?repository_url=local",
64+
"pkg:cargo/crate-b@0.2.0?repository_url=local"
6565
]
6666
},
6767
{
68-
"ref": "pkg:cargo/crate-a@0.1.0",
68+
"ref": "pkg:cargo/crate-a@0.1.0?repository_url=local",
6969
"dependsOn": [
7070
"pkg:cargo/serde@1.0.193"
7171
]
@@ -75,7 +75,7 @@
7575
"dependsOn": []
7676
},
7777
{
78-
"ref": "pkg:cargo/crate-b@0.2.0",
78+
"ref": "pkg:cargo/crate-b@0.2.0?repository_url=local",
7979
"dependsOn": [
8080
"pkg:cargo/tokio@1.35.0"
8181
]

test/providers/tst_manifests/cargo/cargo_virtual_workspace_glob_members/expected_sbom_stack_analysis.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@
2323
{
2424
"name": "crate-a",
2525
"version": "0.1.0",
26-
"purl": "pkg:cargo/crate-a@0.1.0",
26+
"purl": "pkg:cargo/crate-a@0.1.0?repository_url=local",
2727
"type": "library",
28-
"bom-ref": "pkg:cargo/crate-a@0.1.0"
28+
"bom-ref": "pkg:cargo/crate-a@0.1.0?repository_url=local"
2929
},
3030
{
3131
"name": "crate-b",
3232
"version": "0.2.0",
33-
"purl": "pkg:cargo/crate-b@0.2.0",
33+
"purl": "pkg:cargo/crate-b@0.2.0?repository_url=local",
3434
"type": "library",
35-
"bom-ref": "pkg:cargo/crate-b@0.2.0"
35+
"bom-ref": "pkg:cargo/crate-b@0.2.0?repository_url=local"
3636
},
3737
{
3838
"name": "tokio",
@@ -53,16 +53,16 @@
5353
{
5454
"ref": "pkg:cargo/cargo_virtual_workspace_glob_members@0.0.0",
5555
"dependsOn": [
56-
"pkg:cargo/crate-a@0.1.0",
57-
"pkg:cargo/crate-b@0.2.0"
56+
"pkg:cargo/crate-a@0.1.0?repository_url=local",
57+
"pkg:cargo/crate-b@0.2.0?repository_url=local"
5858
]
5959
},
6060
{
61-
"ref": "pkg:cargo/crate-a@0.1.0",
61+
"ref": "pkg:cargo/crate-a@0.1.0?repository_url=local",
6262
"dependsOn": []
6363
},
6464
{
65-
"ref": "pkg:cargo/crate-b@0.2.0",
65+
"ref": "pkg:cargo/crate-b@0.2.0?repository_url=local",
6666
"dependsOn": [
6767
"pkg:cargo/tokio@1.35.0"
6868
]

test/providers/tst_manifests/cargo/cargo_virtual_workspace_with_exhortignore/expected_sbom_stack_analysis.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@
2323
{
2424
"name": "crate-a",
2525
"version": "0.1.0",
26-
"purl": "pkg:cargo/crate-a@0.1.0",
26+
"purl": "pkg:cargo/crate-a@0.1.0?repository_url=local",
2727
"type": "library",
28-
"bom-ref": "pkg:cargo/crate-a@0.1.0"
28+
"bom-ref": "pkg:cargo/crate-a@0.1.0?repository_url=local"
2929
},
3030
{
3131
"name": "crate-b",
3232
"version": "0.2.0",
33-
"purl": "pkg:cargo/crate-b@0.2.0",
33+
"purl": "pkg:cargo/crate-b@0.2.0?repository_url=local",
3434
"type": "library",
35-
"bom-ref": "pkg:cargo/crate-b@0.2.0"
35+
"bom-ref": "pkg:cargo/crate-b@0.2.0?repository_url=local"
3636
},
3737
{
3838
"name": "tokio",
@@ -53,16 +53,16 @@
5353
{
5454
"ref": "pkg:cargo/cargo_virtual_workspace_with_exhortignore@0.0.0",
5555
"dependsOn": [
56-
"pkg:cargo/crate-a@0.1.0",
57-
"pkg:cargo/crate-b@0.2.0"
56+
"pkg:cargo/crate-a@0.1.0?repository_url=local",
57+
"pkg:cargo/crate-b@0.2.0?repository_url=local"
5858
]
5959
},
6060
{
61-
"ref": "pkg:cargo/crate-a@0.1.0",
61+
"ref": "pkg:cargo/crate-a@0.1.0?repository_url=local",
6262
"dependsOn": []
6363
},
6464
{
65-
"ref": "pkg:cargo/crate-b@0.2.0",
65+
"ref": "pkg:cargo/crate-b@0.2.0?repository_url=local",
6666
"dependsOn": [
6767
"pkg:cargo/tokio@1.35.0"
6868
]

test/providers/tst_manifests/cargo/cargo_virtual_workspace_with_ignore/expected_sbom_stack_analysis.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,16 @@
2323
{
2424
"name": "crate-a",
2525
"version": "0.1.0",
26-
"purl": "pkg:cargo/crate-a@0.1.0",
26+
"purl": "pkg:cargo/crate-a@0.1.0?repository_url=local",
2727
"type": "library",
28-
"bom-ref": "pkg:cargo/crate-a@0.1.0"
28+
"bom-ref": "pkg:cargo/crate-a@0.1.0?repository_url=local"
2929
},
3030
{
3131
"name": "crate-b",
3232
"version": "0.2.0",
33-
"purl": "pkg:cargo/crate-b@0.2.0",
33+
"purl": "pkg:cargo/crate-b@0.2.0?repository_url=local",
3434
"type": "library",
35-
"bom-ref": "pkg:cargo/crate-b@0.2.0"
35+
"bom-ref": "pkg:cargo/crate-b@0.2.0?repository_url=local"
3636
},
3737
{
3838
"name": "tokio",
@@ -53,16 +53,16 @@
5353
{
5454
"ref": "pkg:cargo/cargo_virtual_workspace_with_ignore@0.0.0",
5555
"dependsOn": [
56-
"pkg:cargo/crate-a@0.1.0",
57-
"pkg:cargo/crate-b@0.2.0"
56+
"pkg:cargo/crate-a@0.1.0?repository_url=local",
57+
"pkg:cargo/crate-b@0.2.0?repository_url=local"
5858
]
5959
},
6060
{
61-
"ref": "pkg:cargo/crate-a@0.1.0",
61+
"ref": "pkg:cargo/crate-a@0.1.0?repository_url=local",
6262
"dependsOn": []
6363
},
6464
{
65-
"ref": "pkg:cargo/crate-b@0.2.0",
65+
"ref": "pkg:cargo/crate-b@0.2.0?repository_url=local",
6666
"dependsOn": [
6767
"pkg:cargo/tokio@1.35.0"
6868
]

test/providers/tst_manifests/cargo/cargo_virtual_workspace_with_license/expected_sbom_stack_analysis.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@
3737
{
3838
"name": "crate-a",
3939
"version": "0.1.0",
40-
"purl": "pkg:cargo/crate-a@0.1.0",
40+
"purl": "pkg:cargo/crate-a@0.1.0?repository_url=local",
4141
"type": "library",
42-
"bom-ref": "pkg:cargo/crate-a@0.1.0"
42+
"bom-ref": "pkg:cargo/crate-a@0.1.0?repository_url=local"
4343
},
4444
{
4545
"name": "serde",
@@ -51,9 +51,9 @@
5151
{
5252
"name": "crate-b",
5353
"version": "0.2.0",
54-
"purl": "pkg:cargo/crate-b@0.2.0",
54+
"purl": "pkg:cargo/crate-b@0.2.0?repository_url=local",
5555
"type": "library",
56-
"bom-ref": "pkg:cargo/crate-b@0.2.0"
56+
"bom-ref": "pkg:cargo/crate-b@0.2.0?repository_url=local"
5757
},
5858
{
5959
"name": "tokio",
@@ -74,12 +74,12 @@
7474
{
7575
"ref": "pkg:cargo/cargo_virtual_workspace_with_license@0.0.0",
7676
"dependsOn": [
77-
"pkg:cargo/crate-a@0.1.0",
78-
"pkg:cargo/crate-b@0.2.0"
77+
"pkg:cargo/crate-a@0.1.0?repository_url=local",
78+
"pkg:cargo/crate-b@0.2.0?repository_url=local"
7979
]
8080
},
8181
{
82-
"ref": "pkg:cargo/crate-a@0.1.0",
82+
"ref": "pkg:cargo/crate-a@0.1.0?repository_url=local",
8383
"dependsOn": [
8484
"pkg:cargo/serde@1.0.193"
8585
]
@@ -89,7 +89,7 @@
8989
"dependsOn": []
9090
},
9191
{
92-
"ref": "pkg:cargo/crate-b@0.2.0",
92+
"ref": "pkg:cargo/crate-b@0.2.0?repository_url=local",
9393
"dependsOn": [
9494
"pkg:cargo/tokio@1.35.0"
9595
]

0 commit comments

Comments
 (0)