107107from cyclonedx .model .license import LicenseAcknowledgement
108108from cyclonedx .output import make_outputter
109109from cyclonedx .schema import OutputFormat , SchemaVersion
110+ from packageurl import PackageURL
110111
111112import dfetch .util .purl
112- from dfetch .util .purl import DFETCH_TO_CDX_HASH_ALGORITHM
113113from dfetch .manifest .manifest import Manifest
114114from dfetch .manifest .project import ProjectEntry
115115from dfetch .reporting .reporter import Reporter
116116from dfetch .util .license import License
117+ from dfetch .util .purl import DFETCH_TO_CDX_HASH_ALGORITHM
117118
118119# PyRight is pedantic with decorators see https://github.com/madpah/serializable/issues/8
119120# It might be fixable with https://github.com/microsoft/pyright/discussions/4426, would prefer
@@ -190,11 +191,8 @@ def add_project(
190191 purl = dfetch .util .purl .remote_url_to_purl (
191192 project .remote_url , version = version , subpath = project .source or None
192193 )
193-
194194 name = project .name if purl .type == "generic" else purl .name
195-
196195 location = self .manifest .find_name_in_manifest (project .name )
197-
198196 component = Component (
199197 name = name ,
200198 version = version ,
@@ -250,7 +248,15 @@ def add_project(
250248 ],
251249 ),
252250 )
251+ self ._apply_external_references (component , purl , version )
252+ self ._apply_licenses (component , licenses )
253+ self ._bom .components .add (component )
253254
255+ @staticmethod
256+ def _apply_external_references (
257+ component : Component , purl : PackageURL , version : str
258+ ) -> None :
259+ """Attach external references to *component* based on its PURL type."""
254260 if purl .type == "github" :
255261 component .external_references .add (
256262 ExternalReference (
@@ -266,53 +272,62 @@ def add_project(
266272 )
267273 )
268274 elif purl .qualifiers .get ("download_url" ):
269- # Archive dependency: add a DISTRIBUTION external reference and,
270- # when the version encodes a cryptographic hash, record it on the component.
271- download_url = purl .qualifiers ["download_url" ]
272- component .group = purl .namespace or None # type: ignore[assignment]
275+ SbomReporter ._apply_archive_refs (component , purl , version )
276+ else :
277+ SbomReporter ._apply_vcs_refs (component , purl )
278+
279+ @staticmethod
280+ def _apply_archive_refs (
281+ component : Component , purl : PackageURL , version : str
282+ ) -> None :
283+ """Add DISTRIBUTION reference and optional hash for an archive dependency."""
284+ download_url = purl .qualifiers ["download_url" ]
285+ component .group = purl .namespace or None # type: ignore[assignment]
286+ component .external_references .add (
287+ ExternalReference (
288+ type = ExternalReferenceType .DISTRIBUTION ,
289+ url = XsUri (download_url ),
290+ )
291+ )
292+ if version and ":" in version :
293+ algo_prefix , hex_value = version .split (":" , 1 )
294+ cdx_algo_name = DFETCH_TO_CDX_HASH_ALGORITHM .get (algo_prefix )
295+ if cdx_algo_name :
296+ component .hashes .add (
297+ HashType (
298+ alg = HashAlgorithm (cdx_algo_name ),
299+ content = hex_value ,
300+ )
301+ )
302+
303+ @staticmethod
304+ def _apply_vcs_refs (component : Component , purl : PackageURL ) -> None :
305+ """Add VCS external reference and group for a generic VCS dependency."""
306+ component .group = purl .namespace
307+ vcs_url = purl .qualifiers .get ("vcs_url" , "" )
308+ # ExternalReferenceType.VCS does not support ssh:// urls
309+ if vcs_url and "ssh://" not in vcs_url :
273310 component .external_references .add (
274311 ExternalReference (
275- type = ExternalReferenceType .DISTRIBUTION ,
276- url = XsUri (download_url ),
312+ type = ExternalReferenceType .VCS ,
313+ url = XsUri (vcs_url ),
277314 )
278315 )
279- if version and ":" in version :
280- algo_prefix , hex_value = version .split (":" , 1 )
281- cdx_algo_name = DFETCH_TO_CDX_HASH_ALGORITHM .get (algo_prefix )
282- if cdx_algo_name :
283- component .hashes .add (
284- HashType (
285- alg = HashAlgorithm (cdx_algo_name ),
286- content = hex_value ,
287- )
288- )
289- else :
290- component .group = purl .namespace
291-
292- vcs_url = purl .qualifiers .get ("vcs_url" , "" )
293- # ExternalReferenceType.VCS does not support ssh:// urls
294- if vcs_url and "ssh://" not in vcs_url :
295- component .external_references .add (
296- ExternalReference (
297- type = ExternalReferenceType .VCS ,
298- url = XsUri (vcs_url ),
299- )
300- )
301316
317+ @staticmethod
318+ def _apply_licenses (component : Component , licenses : list [License ]) -> None :
319+ """Attach *licenses* to *component* and its evidence block."""
302320 for lic in licenses :
303- # License wants either an SPDX id or a name, prefer SPDX id when available
321+ # Prefer SPDX id when available
304322 cdx_license = (
305323 CycloneDxLicense (id = lic .spdx_id )
306324 if lic .spdx_id
307325 else CycloneDxLicense (name = lic .name )
308326 )
309-
310327 component .licenses .add (cdx_license )
311328 if component .evidence :
312329 component .evidence .licenses .add (cdx_license )
313330
314- self ._bom .components .add (component )
315-
316331 def dump_to_file (self , outfile : str ) -> bool :
317332 """Dump the SBoM to file."""
318333 output_format = OutputFormat (
0 commit comments