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
8 changes: 4 additions & 4 deletions sbomify_action/_enrichment/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ class NormalizedMetadata:

# Distribution info (BSI TR-03183-2 compliance)
distribution_filename: Optional[str] = None
# Hashes of the deployable artefact, keyed by algorithm name as the
# Hashes of the deployable artifact, keyed by algorithm name as the
# source provided it. Keys are lower-case strings — typically SPDX /
# CycloneDX canonical names such as "sha256", "sha512", "md5", but
# some sources legitimately use their own variant spelling. The PyPI
# source, for instance, returns BLAKE2b-256 under the underscore
# form "blake2b_256" (matching PyPI's JSON API), and we keep that
# form here so the downstream algorithm map (_CYCLONEDX_HASH_ALGORITHMS /
# _SPDX_CHECKSUM_ALGORITHMS in enrichment.py) can recognise it
# without a second normalisation step. Values are lower-case hex
# _SPDX_CHECKSUM_ALGORITHMS in enrichment.py) can recognize it
# without a second normalization step. Values are lower-case hex
# strings. Used to populate NTIA / BSI §5.2.2 / CISA "Component
# Hash" elements.
hashes: Dict[str, str] = field(default_factory=dict)
Expand Down Expand Up @@ -102,7 +102,7 @@ def pick(field_name: str, self_val: Any, other_val: Any, is_list: bool = False)
# conflicting algorithms, but every algorithm contributed by
# `other` is preserved. Dropping other's keys would silently lose
# useful digests when PyPI gives sha256 and another source
# contributes blake2b / md5 for the same artefact.
# contributes blake2b / md5 for the same artifact.
#
# Attribution for the union field: if `other` adds at least one
# new algorithm key, refresh ``field_sources["hashes"]`` to
Expand Down
4 changes: 2 additions & 2 deletions sbomify_action/_enrichment/sources/pypi.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def fetch(self, purl: PackageURL, session: requests.Session) -> Optional[Normali
# 3. Build the cache key from the now-validated name/version.
#
# Path-traversal rules:
# - Names: PEP 503 allows "." and "_" inside a normalised project
# - Names: PEP 503 allows "." and "_" inside a normalized project
# name (so "a..b" is a legal-but-unusual name). Reject only
# actual path separators ("/", "\\") and the whole-name
# dot-segments ("." / ".."). The ":" character is refused too
Expand Down Expand Up @@ -321,7 +321,7 @@ def _normalize_response(self, package_name: str, data: Dict[str, Any]) -> Normal
# (BSI TR-03183-2 §5.2.2 filename + hash / NTIA / CISA hash element).
# Prefer wheel (.whl) over sdist (.tar.gz); take the hashes of the
# distribution whose filename we record so the two fields describe
# the same artefact.
# the same artifact.
distribution_filename = None
distribution_hashes: Dict[str, str] = {}
urls = data.get("urls", [])
Expand Down
2 changes: 1 addition & 1 deletion sbomify_action/_generation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ def log_progress() -> None:
)
except subprocess.TimeoutExpired:
elapsed = int(time.time() - start_time)
# Honour log_errors here too: a cdxgen timeout on, say, a Python
# Honor log_errors here too: a cdxgen timeout on, say, a Python
# lockfile is the same benign priority-chain fallback as a non-zero
# exit — it shouldn't spam red ERROR when a later generator succeeds.
timeout_msg = f"{command_name} command timed out after {elapsed}s (limit: {timeout}s)"
Expand Down
2 changes: 1 addition & 1 deletion sbomify_action/cli/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1236,7 +1236,7 @@ def _write_final_output(src_path: str, dst_path: str, bom_type: Optional[str]) -
"""Write the final artifact to ``dst_path``.

Non-SBOM artifacts (VEX, CBOM, ...) are copied byte-for-byte so the upload matches the
authored file exactly (a text round-trip could normalise newlines). SBOMs go through the
authored file exactly (a text round-trip could normalize newlines). SBOMs go through the
text path so the CycloneDX fixups in :func:`_finalize_output_content` apply.
"""
if bom_type and bom_type != "sbom":
Expand Down
9 changes: 5 additions & 4 deletions sbomify_action/cli/wizard/apply.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ def _per_component_best_effort(
relative-path to error message. Catches only ``APIError`` (which is
the parent of ``AuthError`` per exceptions.py and includes the
ConnectionError/Timeout shims in sbomify_api._request); anything
else escapes and aborts the apply, which is the right behaviour for
else escapes and aborts the apply, which is the right behavior for
programming errors. ``label`` is interpolated into per-failure
warning messages ("Could not {label} for foo (cid): …").
"""
Expand Down Expand Up @@ -549,10 +549,11 @@ def _resolve_product(state: WizardState, log: LogFn) -> dict[str, Any] | None:
assert state.workspace is not None # narrowed by caller

if plan.create_product:
product = api.create_product(plan.create_product)
product, was_created = api.get_or_create_product(plan.create_product)
state.created_product_id = str(product.get("id") or "")
state.applied.append(f"created product {product.get('name')}")
log("success", f"Created product {product.get('name')}")
verb = "Created" if was_created else "Reused existing"
state.applied.append(f"{verb.lower()} product {product.get('name')}")
log("success" if was_created else "info", f"{verb} product {product.get('name')}")
return product

if plan.use_product_id:
Expand Down
6 changes: 3 additions & 3 deletions sbomify_action/cli/wizard/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
# emitted line appends the generating build's version (eg.
# ``# sbomify-action wizard v26.7.0``; older releases wrote a literal
# ``v1``), so ownership detection matches this version-agnostic
# prefix — files stamped by any wizard version stay recognised.
# prefix — files stamped by any wizard version stay recognized.
WIZARD_HEADER_SENTINEL = "# sbomify-action wizard"

# Top-level key on every wizard-generated sbomify.json. Its presence
Expand Down Expand Up @@ -124,8 +124,8 @@ def write_sbomify_json(path: Path, payload: dict[str, Any]) -> None:
Mirrors ``write_workflow``: refuses to overwrite a file that exists
but lacks the wizard sentinel key, so a hand-crafted sbomify.json
is never silently clobbered. The sentinel key is injected before
serialisation; the action's json_config provider ignores unknown
top-level keys so its presence doesn't change runtime behaviour.
serialization; the action's json_config provider ignores unknown
top-level keys so its presence doesn't change runtime behavior.

When the existing wizard-stamped file's content differs from what we're
about to write, the previous version is saved next to it as
Expand Down
23 changes: 23 additions & 0 deletions sbomify_action/cli/wizard/screens/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from __future__ import annotations

import re
from typing import TYPE_CHECKING, Callable, ClassVar

from textual import events
Expand All @@ -28,6 +29,28 @@

TOTAL_STEPS = 8

# HTTP status markers as ``_build_error`` emits them: ``prefix [NNN]`` at the
# end of a clause, or ``prefix [NNN] - detail`` when a detail follows. Anchored
# to that exact shape — the code must be a real HTTP status (1xx–5xx) AND be
# followed by `` - `` or the end of the string — so an embedded, quoted name
# like ``'Widget [123]'`` (the ``[123]`` is followed by ``'``, not `` - ``/end)
# is left untouched.
_STATUS_CODE_RE = re.compile(r"\s*\[[1-5]\d{2}\]\s*(?:-\s*|$)")


def strip_status_codes(message: str) -> str:
"""Remove HTTP status-code markers from an API error message.

``"Failed to create product 'X'. [403] - You have reached…"`` becomes
``"Failed to create product 'X'. You have reached…"``. Full error text
(codes included) still lands in the debug log via the exception itself;
this only cleans what the TUI renders. Only markers in the shape
``_build_error`` produces are stripped, so a bracketed number inside a
product/component name isn't mangled.
"""
return _STATUS_CODE_RE.sub(" ", message).strip()


# Responsive breakpoints (terminal cells). The wizard is a fit-to-viewport
# TUI — nothing scrolls — so every screen has to render inside whatever the
# terminal gives us. Three tiers, each guaranteed to fit at its lower bound:
Expand Down
Loading