@@ -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) {
626629function 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+ }
0 commit comments