|
108 | 108 | """ |
109 | 109 |
|
110 | 110 | from decimal import Decimal |
| 111 | +from importlib.metadata import PackageNotFoundError |
111 | 112 | from importlib.metadata import version as pkg_version |
112 | 113 |
|
113 | 114 | from cyclonedx.builder.this import this_component as cdx_lib_component |
|
140 | 141 | from dfetch.manifest.manifest import Manifest |
141 | 142 | from dfetch.manifest.project import ProjectEntry |
142 | 143 | from dfetch.reporting.reporter import Reporter |
| 144 | +from dfetch.util.license import License as DfetchLicense |
143 | 145 | from dfetch.util.license import LicenseScanResult |
144 | 146 | from dfetch.util.purl import vcs_url_to_purl |
145 | 147 | from dfetch.vcs.archive import archive_url_to_purl, is_archive_url |
|
152 | 154 |
|
153 | 155 |
|
154 | 156 | #: Version of the *infer-license* library used for license text analysis. |
155 | | -INFER_LICENSE_VERSION: str = pkg_version("infer-license") |
| 157 | +try: |
| 158 | + INFER_LICENSE_VERSION: str = pkg_version("infer-license") |
| 159 | +except PackageNotFoundError: |
| 160 | + INFER_LICENSE_VERSION = "unknown" |
156 | 161 |
|
157 | 162 | # Map from dfetch hash-field algorithm prefix to CycloneDX HashAlgorithm name |
158 | 163 | DFETCH_TO_CDX_HASH_ALGORITHM: dict[str, str] = { |
@@ -361,6 +366,28 @@ def _apply_vcs_refs(component: Component, purl: PackageURL) -> None: |
361 | 366 | ) |
362 | 367 | ) |
363 | 368 |
|
| 369 | + @staticmethod |
| 370 | + def _attach_identified_licenses( |
| 371 | + component: Component, identified: list[DfetchLicense] |
| 372 | + ) -> None: |
| 373 | + """Add each identified license to *component* and record its confidence.""" |
| 374 | + for lic in identified: |
| 375 | + cdx_license = ( |
| 376 | + CycloneDxLicense(id=lic.spdx_id) |
| 377 | + if lic.spdx_id |
| 378 | + else CycloneDxLicense(name=lic.name) |
| 379 | + ) |
| 380 | + component.licenses.add(cdx_license) |
| 381 | + if component.evidence: |
| 382 | + component.evidence.licenses.add(cdx_license) |
| 383 | + label = lic.spdx_id or lic.name or "unknown" |
| 384 | + component.properties.add( |
| 385 | + Property( |
| 386 | + name=f"dfetch:license:{label}:confidence", |
| 387 | + value=f"{lic.probability:.2f}", |
| 388 | + ) |
| 389 | + ) |
| 390 | + |
364 | 391 | @staticmethod |
365 | 392 | def _apply_licenses(component: Component, license_scan: LicenseScanResult) -> None: |
366 | 393 | """Attach license information to *component* and its evidence block. |
@@ -397,38 +424,28 @@ def _apply_licenses(component: Component, license_scan: LicenseScanResult) -> No |
397 | 424 | ) |
398 | 425 |
|
399 | 426 | if license_scan.identified: |
400 | | - for lic in license_scan.identified: |
401 | | - cdx_license = ( |
402 | | - CycloneDxLicense(id=lic.spdx_id) |
403 | | - if lic.spdx_id |
404 | | - else CycloneDxLicense(name=lic.name) |
405 | | - ) |
406 | | - component.licenses.add(cdx_license) |
407 | | - if component.evidence: |
408 | | - component.evidence.licenses.add(cdx_license) |
409 | | - # Record per-license confidence so auditors can compare against |
410 | | - # the threshold and re-evaluate if the threshold changes. |
411 | | - label = lic.spdx_id or lic.name or "unknown" |
412 | | - component.properties.add( |
413 | | - Property( |
414 | | - name=f"dfetch:license:{label}:confidence", |
415 | | - value=f"{lic.probability:.2f}", |
416 | | - ) |
417 | | - ) |
418 | | - return |
419 | | - |
420 | | - noassertion = CycloneDxLicense(name="NOASSERTION") |
421 | | - component.licenses.add(noassertion) |
422 | | - if component.evidence: |
423 | | - component.evidence.licenses.add(noassertion) |
| 427 | + SbomReporter._attach_identified_licenses(component, license_scan.identified) |
| 428 | + else: |
| 429 | + noassertion = CycloneDxLicense(name="NOASSERTION") |
| 430 | + component.licenses.add(noassertion) |
| 431 | + if component.evidence: |
| 432 | + component.evidence.licenses.add(noassertion) |
424 | 433 |
|
425 | 434 | if license_scan.unclassified_files: |
426 | 435 | files_str = ", ".join(sorted(license_scan.unclassified_files)) |
427 | | - finding = f"License file(s) found ({files_str}) but could not be classified" |
428 | | - else: |
429 | | - finding = "No license file found in source tree" |
430 | | - |
431 | | - component.properties.add(Property(name="dfetch:license:finding", value=finding)) |
| 436 | + component.properties.add( |
| 437 | + Property( |
| 438 | + name="dfetch:license:finding", |
| 439 | + value=f"License file(s) found ({files_str}) but could not be classified", |
| 440 | + ) |
| 441 | + ) |
| 442 | + elif not license_scan.identified: |
| 443 | + component.properties.add( |
| 444 | + Property( |
| 445 | + name="dfetch:license:finding", |
| 446 | + value="No license file found in source tree", |
| 447 | + ) |
| 448 | + ) |
432 | 449 |
|
433 | 450 | def dump_to_file(self, outfile: str) -> bool: |
434 | 451 | """Dump the SBoM to file.""" |
|
0 commit comments