Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/reference/config-reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ For example `flash_attn.yaml`.

.. autopydantic_model:: fromager.packagesettings.SbomSettings

SBOM upstream purls
^^^^^^^^^^^^^^^^^^^

When ``purl.upstream`` is absent, the upstream SBOM entry identifies the
resolved source as ``pkg:pypi/<package name>@<resolved version>``. The
per-package ``purl.name``, ``purl.type``, and ``purl.namespace`` settings and
Comment thread
mprpic marked this conversation as resolved.
Resolved
the global ``sbom.purl_type`` setting affect the wheel entry only. Previously,
those settings also changed the upstream entry. Set ``purl.upstream`` to the
full source purl when the source is not the resolved PyPI package.

Source Resolver
^^^^^^^^^^^^^^^

Expand Down
9 changes: 5 additions & 4 deletions src/fromager/packagesettings/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class SbomSettings(pydantic.BaseModel):
"""

purl_type: PurlType = "pypi"
"""Default purl type for all packages (e.g. ``pypi``, ``generic``)"""
"""Default purl type for wheel package entries (e.g. ``pypi``, ``generic``)"""

repository_url: AnyUrl | None = None
"""Default purl ``repository_url`` qualifier for all packages
Expand Down Expand Up @@ -343,9 +343,10 @@ class PurlConfig(pydantic.BaseModel):
GENERATED_FROM relationship. Used for packages sourced from
GitHub/GitLab rather than PyPI.

When absent, the upstream purl keeps the downstream name, type, and
namespace but uses the resolved source version instead of a local
downstream version override. The ``repository_url`` qualifier is omitted.
When absent, the upstream purl is ``pkg:pypi/<package name>@<resolved
Comment thread
mprpic marked this conversation as resolved.
Resolved
version>``. Wheel-only name, type, namespace, and version overrides and
the global ``sbom.purl_type`` do not change it. For non-PyPI sources,
set ``purl.upstream`` explicitly.
"""


Expand Down
16 changes: 5 additions & 11 deletions src/fromager/sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,26 +62,21 @@ def _build_upstream_purl(
name: NormalizedName,
version: Version,
pbi: PackageBuildInfo,
sbom_settings: SbomSettings,
) -> PackageURL:
"""Build the upstream source package URL.

If ``upstream`` is set in the per-package ``PurlConfig``, it is
used as-is. Otherwise, the upstream purl uses the resolved source
version rather than a downstream wheel version override, and excludes
the ``repository_url`` qualifier.
used as-is. Otherwise, the source is identified by its resolved
PyPI name and version, regardless of downstream wheel purl overrides.
"""
pc = pbi.purl_config
if pc and pc.upstream:
return PackageURL.from_string(pc.upstream)

purl_type = pc.type if pc else None
purl_namespace = pc.namespace if pc else None
purl_name = pc.name if pc else None
return PackageURL(
Comment thread
mprpic marked this conversation as resolved.
Resolved
type=purl_type or sbom_settings.purl_type,
namespace=purl_namespace,
name=purl_name or name,
type="pypi",
namespace=None,
name=name,
version=str(version),
)

Expand Down Expand Up @@ -132,7 +127,6 @@ def generate_sbom(
name=name,
version=version,
pbi=pbi,
sbom_settings=sbom_settings,
)

# Derive the document identity from the downstream purl so the
Expand Down
42 changes: 39 additions & 3 deletions tests/test_sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import uuid
from unittest.mock import patch

import pytest
from conftest import make_sbom_ctx
from packaging.requirements import Requirement
from packaging.version import Version
Expand Down Expand Up @@ -196,14 +197,49 @@ def test_generate_sbom_wheel_version_used_without_purl_override(
assert wheel["externalRefs"][0]["referenceLocator"] == (
"pkg:pypi/test-pkg@0.5.5%2Blocal.1"
)
# Upstream still identifies the resolved source release
assert upstream["versionInfo"] == "0.5.5"
assert upstream["externalRefs"][0]["referenceLocator"] == (
"pkg:pypi/test-pkg@0.5.5"
)
_validate_spdx(doc)


@pytest.mark.parametrize(
("package_purl", "sbom_type", "expected_wheel"),
[
({"name": "test-pkg-corp"}, "pypi", "pkg:pypi/test-pkg-corp@0.5.5"),
({"type": "generic"}, "pypi", "pkg:generic/test-pkg@0.5.5"),
({"namespace": "acme"}, "pypi", "pkg:pypi/acme/test-pkg@0.5.5"),
Comment thread
mprpic marked this conversation as resolved.
Resolved
({}, "generic", "pkg:generic/test-pkg@0.5.5"),
],
)
def test_generate_sbom_upstream_ignores_wheel_purl_overrides(
tmp_path: pathlib.Path,
package_purl: dict[str, str],
sbom_type: str,
expected_wheel: str,
) -> None:
"""Wheel-only purl settings must not rewrite the source identity."""
ctx = make_sbom_ctx(
tmp_path,
sbom_settings=SbomSettings(purl_type=sbom_type),
package_overrides={"purl": package_purl},
)
doc = sbom.generate_sbom(
ctx=ctx,
req=Requirement("test-pkg==0.5.5"),
version=Version("0.5.5"),
)

wheel, upstream = doc["packages"]
assert wheel["externalRefs"][0]["referenceLocator"] == expected_wheel
assert upstream["name"] == "test-pkg"
assert upstream["externalRefs"][0]["referenceLocator"] == (
"pkg:pypi/test-pkg@0.5.5"
)
_validate_spdx(doc)


def test_generate_sbom_purl_version_override_wins_over_wheel_version(
tmp_path: pathlib.Path,
) -> None:
Expand Down Expand Up @@ -297,9 +333,9 @@ def test_generate_sbom_purl_field_overrides(tmp_path: pathlib.Path) -> None:
assert wheel["externalRefs"][0]["referenceLocator"] == (
"pkg:generic/custom-name@1.0.0"
)
# Field overrides carry through to upstream (without qualifiers)
# Wheel field overrides must not change the resolved source identity.
assert upstream["externalRefs"][0]["referenceLocator"] == (
"pkg:generic/custom-name@1.0.0"
"pkg:pypi/test-pkg@1.0.0"
)
# Document identity follows the purl name override
assert doc["name"] == "custom-name-1.0.0"
Expand Down
Loading