diff --git a/ci/build-release-candidate.py b/ci/build-release-candidate.py index 3ee4f78..968c644 100644 --- a/ci/build-release-candidate.py +++ b/ci/build-release-candidate.py @@ -406,7 +406,18 @@ def normalize_and_validate_sbom( raise CandidateError( f"CycloneDX component lacks a license: {component.get('name')!r}" ) - if not component_has_sha256(component): + # WHY the hash rule splits on inventory membership: an inventory + # component already carries every digest its hash source can honestly + # provide -- validated_component_inventory refuses a repository-file or + # external-distribution entry without one, while a registry-version-pin + # entry records that no upstream per-install integrity value exists + # (ci/tool-lock.toml states the same contract). Demanding a SHA-256 for + # those here would require a digest that cannot exist; a component from + # any other source still must carry one. + if ( + not component_has_sha256(component) + and str(component.get("purl")) not in inventory_purls + ): raise CandidateError( f"CycloneDX component lacks a SHA-256 hash: {component.get('name')!r}" ) diff --git a/ci/check-release-candidate.py b/ci/check-release-candidate.py index 30d89c0..c83a6cb 100644 --- a/ci/check-release-candidate.py +++ b/ci/check-release-candidate.py @@ -110,6 +110,17 @@ def main() -> int: "sha256": "1" * 64, }, }, + { + "name": "Fixture registry pin", + "version": "3.0.0", + "purl": "pkg:npm/fixture-registry-pin@3.0.0", + "scope": "required", + "license": "MIT", + "hash": { + "kind": "registry-version-pin", + "registry": "npm", + }, + }, ], }, indent=2, @@ -180,7 +191,14 @@ def main() -> int: assert {component["purl"] for component in sbom_doc["components"]} == { "pkg:generic/fixture-font@1.0.0?file_name=fixture.woff2", "pkg:generic/fixture-renderer@2.0.0?arch=x86_64&os=linux", + "pkg:npm/fixture-registry-pin@3.0.0", } + registry_pin = next( + component + for component in sbom_doc["components"] + if component["purl"] == "pkg:npm/fixture-registry-pin@3.0.0" + ) + assert "hashes" not in registry_pin dependency_rows = { row["ref"]: row["dependsOn"] for row in sbom_doc["dependencies"] }