fix(assessment): establish independent release trust
Some checks failed
Dependency Audit / dependency-audit (push) Failing after 15s
Security Audit / security-audit (push) Failing after 13s

This commit is contained in:
2026-07-22 17:09:35 +02:00
parent 753dd2a3ee
commit ad8dd4f319
7 changed files with 577 additions and 102 deletions

View File

@@ -7,11 +7,12 @@ import json
from pathlib import Path
from urllib.error import HTTPError, URLError
from .http_fetch import fetch_http
from .http_fetch import DEFAULT_MAX_RESPONSE_BYTES, fetch_http
from .model import CatalogSnapshot
from .workspace import website_root
DEFAULT_PUBLIC_BASE_URL = "https://govoplan.add-ideas.de"
MAX_PUBLIC_JSON_RESPONSE_BYTES = DEFAULT_MAX_RESPONSE_BYTES
def collect_catalog_snapshot(
@@ -22,9 +23,13 @@ def collect_catalog_snapshot(
check_public: bool = False,
) -> CatalogSnapshot:
web_root = website_root(workspace_root)
catalog_path = web_root / "public" / "catalogs" / "v1" / "channels" / f"{channel}.json"
catalog_path = (
web_root / "public" / "catalogs" / "v1" / "channels" / f"{channel}.json"
)
keyring_path = web_root / "public" / "catalogs" / "v1" / "keyring.json"
public_catalog_url = f"{public_base_url.rstrip('/')}/catalogs/v1/channels/{channel}.json"
public_catalog_url = (
f"{public_base_url.rstrip('/')}/catalogs/v1/channels/{channel}.json"
)
public_keyring_url = f"{public_base_url.rstrip('/')}/catalogs/v1/keyring.json"
catalog_version: str | None = None
@@ -129,8 +134,12 @@ def collect_catalog_snapshot(
public_keyring_hash=public_keyring_hash,
public_catalog_error=public_catalog_error,
public_keyring_error=public_keyring_error,
catalog_matches_public=compare_hashes(local_catalog_hash, public_catalog_hash) if check_public else None,
keyring_matches_public=compare_hashes(local_keyring_hash, public_keyring_hash) if check_public else None,
catalog_matches_public=compare_hashes(local_catalog_hash, public_catalog_hash)
if check_public
else None,
keyring_matches_public=compare_hashes(local_keyring_hash, public_keyring_hash)
if check_public
else None,
)
@@ -149,7 +158,9 @@ def parse_catalog_payload(payload: object) -> dict[str, str | int | None]:
signatures = payload.get("signatures")
return {
"catalog_version": string_or_none(payload.get("catalog_version")),
"core_release_version": string_or_none(core_release.get("version")) if isinstance(core_release, dict) else None,
"core_release_version": string_or_none(core_release.get("version"))
if isinstance(core_release, dict)
else None,
"module_count": len(modules) if isinstance(modules, list) else 0,
"signature_count": len(signatures) if isinstance(signatures, list) else 0,
"generated_at": string_or_none(payload.get("generated_at")),
@@ -164,6 +175,7 @@ def fetch_json(url: str) -> dict[str, object]:
timeout=10,
label="Public release catalog URL",
headers={"User-Agent": "GovOPlaN release-console"},
max_response_bytes=MAX_PUBLIC_JSON_RESPONSE_BYTES,
)
return {"ok": True, "payload": json.loads(response.text())}
except (HTTPError, URLError, TimeoutError, ValueError, json.JSONDecodeError) as exc:
@@ -171,7 +183,9 @@ def fetch_json(url: str) -> dict[str, object]:
def canonical_hash(payload: object) -> str:
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True).encode("utf-8")
canonical = json.dumps(
payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True
).encode("utf-8")
return hashlib.sha256(canonical).hexdigest()

View File

@@ -8,6 +8,9 @@ from dataclasses import dataclass
from typing import Mapping
DEFAULT_MAX_RESPONSE_BYTES = 16 * 1024 * 1024
@dataclass(frozen=True, slots=True)
class HttpFetchResponse:
status: int
@@ -34,11 +37,29 @@ def fetch_http(
label: str = "URL",
method: str = "GET",
headers: Mapping[str, str] | None = None,
max_response_bytes: int = DEFAULT_MAX_RESPONSE_BYTES,
) -> HttpFetchResponse:
request = urllib.request.Request(validate_http_url(url, label=label), headers=dict(headers or {}), method=method)
if max_response_bytes <= 0:
raise ValueError("max_response_bytes must be positive.")
request = urllib.request.Request(
validate_http_url(url, label=label),
headers=dict(headers or {}),
method=method,
)
with urllib.request.urlopen(request, timeout=timeout) as response: # noqa: S310 - URL is validated by validate_http_url. # nosec B310 # nosemgrep: python.lang.security.audit.dynamic-urllib-use-detected.dynamic-urllib-use-detected
raw_length = response.headers.get("Content-Length")
if raw_length is not None:
try:
declared_length = int(raw_length)
except (TypeError, ValueError):
declared_length = None
if declared_length is not None and declared_length > max_response_bytes:
raise ValueError(f"{label} response exceeds the configured size limit.")
body = response.read(max_response_bytes + 1)
if len(body) > max_response_bytes:
raise ValueError(f"{label} response exceeds the configured size limit.")
return HttpFetchResponse(
status=int(getattr(response, "status", 0)),
headers=dict(response.headers.items()),
body=response.read(),
body=body,
)