feat(sbom): add authoritative Nexus client

Assistant: codex
Assistant-Model: gpt-5.6-sol
Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
This commit is contained in:
tegwick 2026-08-22 23:26:45 +02:00
parent 135b1647d7
commit b068e9da42
5 changed files with 446 additions and 4 deletions

View file

@ -6,8 +6,12 @@ import json
import os
import shutil
import subprocess
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from urllib.parse import quote
import httpx
SNAPSHOT_SCHEMA = "sbom-nexus.snapshot.v1"
SNAPSHOT_REQUIRED_FIELDS = frozenset(
@ -32,12 +36,234 @@ PREVIEW_CONTEXT = {
"advances_last_success_at": False,
"creates_snapshot_history": False,
}
SERVICE_CONTEXT = {
"mode": "authoritative-service",
"authoritative": True,
"product_owner": "sbom-nexus",
}
class SBOMContractError(ValueError):
"""Raised when a Nexus response is outside the pinned consumer contract."""
class SBOMServiceError(RuntimeError):
"""A sanitized, deterministic failure from the authoritative service client."""
def __init__(
self,
code: str,
method: str,
path: str,
*,
status_code: int | None = None,
mutation_may_have_committed: bool = False,
) -> None:
self.code = code
self.method = method
self.path = path
self.status_code = status_code
self.mutation_may_have_committed = mutation_may_have_committed
status = f" HTTP {status_code}" if status_code is not None else ""
super().__init__(f"SBOM Nexus {method} {path} failed ({code}{status})")
def to_dict(self) -> dict[str, Any]:
return {
"code": self.code,
"method": self.method,
"path": self.path,
"status_code": self.status_code,
"mutation_may_have_committed": self.mutation_may_have_committed,
}
@dataclass(frozen=True)
class SBOMNexusConfig:
"""Explicit service configuration; credentials are deliberately repr-hidden."""
base_url: str
timeout_seconds: float = 30.0
bearer_token: str | None = field(default=None, repr=False)
def __post_init__(self) -> None:
url = httpx.URL(self.base_url)
if url.scheme not in {"http", "https"} or not url.host:
raise ValueError("SBOM Nexus base_url must be an absolute HTTP(S) URL")
if url.username or url.password or url.query or url.fragment:
raise ValueError("SBOM Nexus base_url must not contain credentials, query, or fragment")
if not 0.1 <= self.timeout_seconds <= 300.0:
raise ValueError("SBOM Nexus timeout_seconds must be between 0.1 and 300")
object.__setattr__(self, "base_url", self.base_url.rstrip("/"))
@classmethod
def from_environment(cls) -> SBOMNexusConfig:
base_url = os.getenv("SBOM_NEXUS_URL")
if not base_url:
raise ValueError("SBOM_NEXUS_URL is required for authoritative service mode")
try:
timeout = float(os.getenv("SBOM_NEXUS_TIMEOUT_SECONDS", "30"))
except ValueError as exc:
raise ValueError("SBOM_NEXUS_TIMEOUT_SECONDS must be numeric") from exc
return cls(
base_url=base_url,
timeout_seconds=timeout,
bearer_token=os.getenv("SBOM_NEXUS_TOKEN") or None,
)
class SBOMNexusClient:
"""Bounded client for Nexus-owned authoritative repository SBOM state."""
def __init__(
self,
config: SBOMNexusConfig,
*,
transport: httpx.BaseTransport | None = None,
) -> None:
self.config = config
self._transport = transport
def upsert_repository(
self,
repo_slug: str,
*,
nexus_checkout_path: str | None,
active: bool = True,
) -> dict[str, Any]:
"""Project identity/path; the path must resolve inside Nexus's controlled source plane."""
return self._request_object(
"PUT",
f"/repositories/{quote(repo_slug, safe='')}",
body={"checkout_path": nexus_checkout_path, "active": active},
operation="repository-projection",
writes_state=True,
required_fields=frozenset({"slug", "active"}),
)
def ingest_repository(
self,
repo_slug: str,
*,
expected_source_revision: str | None = None,
operation_id: str | None = None,
) -> dict[str, Any]:
"""Persist one Nexus scan outcome without automatic mutation retries."""
headers = {"Idempotency-Key": operation_id} if operation_id else None
result = self._request_object(
"POST",
f"/sbom/{quote(repo_slug, safe='')}/ingest",
headers=headers,
operation="ingest",
writes_state=True,
required_fields=frozenset(
{"repo_slug", "snapshot_id", "status", "entry_count", "snapshot_at"}
),
)
if expected_source_revision and result.get("status") == "ingested":
actual = result.get("source_revision")
if actual != expected_source_revision:
raise SBOMServiceError(
"source_revision_mismatch",
"POST",
f"/sbom/{quote(repo_slug, safe='')}/ingest",
mutation_may_have_committed=True,
)
return result
def latest_snapshot(self, repo_slug: str) -> dict[str, Any]:
return self._request_object(
"GET",
f"/sbom/{quote(repo_slug, safe='')}",
operation="latest-snapshot",
writes_state=False,
required_fields=frozenset(
{
"repo_slug",
"last_attempt_at",
"last_success_at",
"last_status",
"entry_count",
"snapshot_id",
"entries",
}
),
)
def licence_report(self) -> dict[str, Any]:
return self._request_object(
"GET",
"/sbom/report/licences/",
operation="licence-report",
writes_state=False,
required_fields=frozenset({"groups", "copyleft_direct_prod", "copyleft_direct_count"}),
)
def _request_object(
self,
method: str,
path: str,
*,
body: dict[str, Any] | None = None,
headers: dict[str, str] | None = None,
operation: str,
writes_state: bool,
required_fields: frozenset[str],
) -> dict[str, Any]:
request_headers = dict(headers or {})
if self.config.bearer_token:
request_headers["Authorization"] = f"Bearer {self.config.bearer_token}"
try:
with httpx.Client(
base_url=self.config.base_url,
timeout=self.config.timeout_seconds,
headers=request_headers,
transport=self._transport,
) as client:
response = client.request(method, path, json=body)
except httpx.TimeoutException as exc:
raise SBOMServiceError("timeout", method, path) from exc
except httpx.RequestError as exc:
raise SBOMServiceError("transport_error", method, path) from exc
if response.status_code >= 400:
raise SBOMServiceError(
"http_error",
method,
path,
status_code=response.status_code,
mutation_may_have_committed=writes_state and response.status_code >= 500,
)
try:
payload = response.json()
except ValueError as exc:
raise SBOMServiceError(
"invalid_json",
method,
path,
mutation_may_have_committed=writes_state,
) from exc
if not isinstance(payload, dict):
raise SBOMServiceError(
"invalid_response_type",
method,
path,
mutation_may_have_committed=writes_state,
)
if required_fields - payload.keys():
raise SBOMServiceError(
"contract_error",
method,
path,
mutation_may_have_committed=writes_state,
)
payload["repo_manager_context"] = {
**SERVICE_CONTEXT,
"operation": operation,
"writes_state": writes_state,
}
return payload
def validate_snapshot_contract(payload: dict[str, Any]) -> None:
"""Accept additive fields but reject unknown schemas or missing required fields."""
if payload.get("schema") != SNAPSHOT_SCHEMA: