857 lines
30 KiB
Python
857 lines
30 KiB
Python
"""Compare a capability-fit assessment with a signed release catalog.
|
|
|
|
The result is deliberately limited to release metadata and local tag
|
|
provenance. It never implies installed-artifact, target-provider, or production
|
|
proof.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import asdict, dataclass
|
|
from datetime import UTC, datetime
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
from typing import Any, Iterable
|
|
from unittest import mock
|
|
|
|
from jsonschema import Draft202012Validator, FormatChecker
|
|
from jsonschema.exceptions import SchemaError
|
|
|
|
import govoplan_core.core.module_package_catalog as module_package_catalog
|
|
from govoplan_release.source_provenance import catalog_source_selection
|
|
from govoplan_release.version_alignment import candidate_catalog_version_issues
|
|
|
|
|
|
RELEASE_REF_PATTERN = re.compile(
|
|
r"^(?P<channel>[a-z][a-z0-9_-]*)-catalog-(?P<sequence>[0-9]+)$"
|
|
)
|
|
TAG_PATTERN = re.compile(r"(?:[.]git[@#])(?P<tag>[^\s]+)$")
|
|
REPOSITORY_ID_PATTERN = re.compile(r"^[a-z0-9][a-z0-9._-]*$")
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Finding:
|
|
severity: str
|
|
code: str
|
|
message: str
|
|
assessment_ids: tuple[str, ...] = ()
|
|
|
|
|
|
def review_capability_fit(
|
|
*,
|
|
assessment: dict[str, Any],
|
|
schema: dict[str, Any],
|
|
catalog: dict[str, Any],
|
|
published_keyring: dict[str, Any],
|
|
trusted_keyring: dict[str, Any],
|
|
workspace_root: Path | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Return a secret-free release-drift report for one fit assessment."""
|
|
|
|
findings: list[Finding] = []
|
|
schema_errors = validate_assessment(assessment=assessment, schema=schema)
|
|
findings.extend(
|
|
Finding("blocker", "assessment_schema", message) for message in schema_errors
|
|
)
|
|
if schema_errors:
|
|
return build_report(
|
|
assessment=assessment,
|
|
catalog=catalog,
|
|
findings=findings,
|
|
changes=[],
|
|
review_targets=[],
|
|
catalog_checked=False,
|
|
local_tag_provenance_attempted=0,
|
|
local_tag_provenance_expected=0,
|
|
local_tag_catalog_scope_complete=False,
|
|
)
|
|
|
|
catalog_validation = validate_catalog(
|
|
catalog=catalog,
|
|
trusted_keyring=trusted_keyring,
|
|
)
|
|
if catalog_validation.get("valid") is not True:
|
|
findings.append(
|
|
Finding(
|
|
"blocker",
|
|
"catalog_trust",
|
|
str(catalog_validation.get("error") or "Catalog validation failed."),
|
|
("assessment.release",),
|
|
)
|
|
)
|
|
findings.extend(
|
|
Finding(
|
|
"blocker",
|
|
"catalog_release_metadata",
|
|
(
|
|
f"{issue.source}: {issue.message}; expected {issue.expected!r}, "
|
|
f"found {issue.actual!r}."
|
|
),
|
|
("assessment.release",),
|
|
)
|
|
for issue in candidate_catalog_version_issues(catalog)
|
|
)
|
|
source_selection = catalog_source_selection(catalog)
|
|
findings.extend(
|
|
Finding(
|
|
"blocker",
|
|
"catalog_source_provenance",
|
|
issue.message,
|
|
("assessment.release",),
|
|
)
|
|
for issue in source_selection.issues
|
|
)
|
|
expected_keyring_hash = _catalog_keyring_hash(catalog)
|
|
actual_keyring_hash = canonical_hash(published_keyring)
|
|
if expected_keyring_hash is None:
|
|
findings.append(
|
|
Finding(
|
|
"blocker",
|
|
"catalog_keyring_hash_missing",
|
|
"Catalog release metadata does not pin the published keyring hash.",
|
|
("assessment.release",),
|
|
)
|
|
)
|
|
elif expected_keyring_hash != actual_keyring_hash:
|
|
findings.append(
|
|
Finding(
|
|
"blocker",
|
|
"catalog_keyring_hash_mismatch",
|
|
"The published keyring does not match the hash pinned by the catalog.",
|
|
("assessment.release",),
|
|
)
|
|
)
|
|
|
|
release = assessment["release"]
|
|
ref_match = RELEASE_REF_PATTERN.fullmatch(str(release["ref"]))
|
|
if ref_match is None:
|
|
findings.append(
|
|
Finding(
|
|
"blocker",
|
|
"assessment_release_ref",
|
|
"Assessment release.ref must have the form '<channel>-catalog-<sequence>'.",
|
|
("assessment.release",),
|
|
)
|
|
)
|
|
assessed_channel = None
|
|
assessed_sequence = None
|
|
else:
|
|
assessed_channel = ref_match.group("channel")
|
|
assessed_sequence = int(ref_match.group("sequence"))
|
|
|
|
catalog_channel = _text(catalog.get("channel"))
|
|
catalog_sequence = _integer(catalog.get("sequence"))
|
|
if assessed_channel is not None and catalog_channel != assessed_channel:
|
|
findings.append(
|
|
Finding(
|
|
"blocker",
|
|
"catalog_channel_mismatch",
|
|
f"Assessment pins channel {assessed_channel!r}, but the supplied catalog is {catalog_channel!r}.",
|
|
("assessment.release",),
|
|
)
|
|
)
|
|
if assessed_sequence is not None and catalog_sequence != assessed_sequence:
|
|
findings.append(
|
|
Finding(
|
|
"review",
|
|
"catalog_sequence_changed",
|
|
f"Assessment pins catalog sequence {assessed_sequence}; supplied catalog sequence is {catalog_sequence!r}.",
|
|
("assessment.release",),
|
|
)
|
|
)
|
|
|
|
catalog_entries, entry_findings = catalog_entries_by_id(catalog)
|
|
findings.extend(entry_findings)
|
|
changes: list[dict[str, Any]] = []
|
|
changed_repositories: set[str] = set()
|
|
assessed_module_ids: set[str] = set()
|
|
composition = assessment["composition"]
|
|
local_tag_provenance_expected = len(composition)
|
|
local_tag_provenance_attempted = 0
|
|
local_tag_catalog_scope_complete = True
|
|
|
|
for module in composition:
|
|
module_id = str(module["module_id"])
|
|
repository = str(module["repository"])
|
|
assessed_version = str(module["manifest_version"])
|
|
selected_version = source_selection.selected_versions.get(repository)
|
|
selected_commit = source_selection.selected_commits.get(repository)
|
|
selected_tag_object = source_selection.selected_tag_objects.get(repository)
|
|
assessed_module_ids.add(module_id)
|
|
if workspace_root is not None:
|
|
local_tag_provenance_attempted += 1
|
|
provenance = local_tag_provenance(
|
|
workspace_root=workspace_root,
|
|
repository=repository,
|
|
version=assessed_version,
|
|
assessed_commit=str(module["commit"]),
|
|
selected_commit=(
|
|
selected_commit if selected_version == assessed_version else None
|
|
),
|
|
selected_tag_object=(
|
|
selected_tag_object
|
|
if selected_version == assessed_version
|
|
else None
|
|
),
|
|
)
|
|
if provenance is not None:
|
|
changes.append(
|
|
{
|
|
"module_id": module_id,
|
|
"repository": repository,
|
|
"kind": provenance["kind"],
|
|
"assessed_commit": module["commit"],
|
|
"tag_commit": provenance.get("tag_commit"),
|
|
"tag_object": provenance.get("tag_object"),
|
|
}
|
|
)
|
|
changed_repositories.add(repository)
|
|
findings.append(
|
|
Finding(
|
|
"review",
|
|
"tag_provenance_changed",
|
|
str(provenance["message"]),
|
|
(f"composition.{module_id}",),
|
|
)
|
|
)
|
|
entry = catalog_entries.get(module_id)
|
|
if entry is None:
|
|
local_tag_catalog_scope_complete = False
|
|
changes.append(
|
|
{
|
|
"module_id": module_id,
|
|
"repository": repository,
|
|
"kind": "missing_from_catalog",
|
|
"assessed_version": module["manifest_version"],
|
|
"catalog_version": None,
|
|
}
|
|
)
|
|
changed_repositories.add(repository)
|
|
findings.append(
|
|
Finding(
|
|
"review",
|
|
"composition_entry_missing",
|
|
f"Assessed module {module_id!r} is not present in the supplied catalog.",
|
|
(f"composition.{module_id}",),
|
|
)
|
|
)
|
|
continue
|
|
|
|
catalog_repository = entry["repository"]
|
|
if catalog_repository != repository:
|
|
changes.append(
|
|
{
|
|
"module_id": module_id,
|
|
"repository": repository,
|
|
"kind": "repository_changed",
|
|
"assessed_repository": repository,
|
|
"catalog_repository": catalog_repository,
|
|
}
|
|
)
|
|
changed_repositories.add(repository)
|
|
findings.append(
|
|
Finding(
|
|
"review",
|
|
"composition_repository_changed",
|
|
f"Module {module_id!r} maps to {catalog_repository!r} in the catalog, not {repository!r}.",
|
|
(f"composition.{module_id}",),
|
|
)
|
|
)
|
|
|
|
catalog_version = entry["version"]
|
|
if assessed_version != catalog_version:
|
|
changes.append(
|
|
{
|
|
"module_id": module_id,
|
|
"repository": repository,
|
|
"kind": "version_changed",
|
|
"assessed_version": assessed_version,
|
|
"catalog_version": catalog_version,
|
|
}
|
|
)
|
|
changed_repositories.add(repository)
|
|
findings.append(
|
|
Finding(
|
|
"review",
|
|
"composition_version_changed",
|
|
f"Module {module_id!r} changed from {assessed_version!r} to {catalog_version!r}.",
|
|
(f"composition.{module_id}",),
|
|
)
|
|
)
|
|
|
|
assessed_commit = str(module["commit"]).lower()
|
|
if (
|
|
selected_version == assessed_version
|
|
and selected_commit is not None
|
|
and not selected_commit.lower().startswith(assessed_commit)
|
|
):
|
|
changes.append(
|
|
{
|
|
"module_id": module_id,
|
|
"repository": repository,
|
|
"kind": "commit_changed",
|
|
"assessed_commit": module["commit"],
|
|
"catalog_commit": selected_commit,
|
|
}
|
|
)
|
|
changed_repositories.add(repository)
|
|
findings.append(
|
|
Finding(
|
|
"review",
|
|
"composition_commit_changed",
|
|
(
|
|
f"Signed selected-unit provenance for {repository!r} at "
|
|
f"{assessed_version!r} points to commit {selected_commit[:12]}, "
|
|
f"not assessed commit {module['commit']}."
|
|
),
|
|
(f"composition.{module_id}",),
|
|
)
|
|
)
|
|
|
|
expected_tag = f"v{catalog_version}" if catalog_version else None
|
|
catalog_tags = entry["tags"]
|
|
if (
|
|
expected_tag is None
|
|
or not catalog_tags
|
|
or any(tag != expected_tag for tag in catalog_tags)
|
|
):
|
|
findings.append(
|
|
Finding(
|
|
"blocker",
|
|
"catalog_ref_version_mismatch",
|
|
f"Catalog refs for {module_id!r} do not consistently select expected tag {expected_tag!r}: {catalog_tags!r}.",
|
|
(f"composition.{module_id}",),
|
|
)
|
|
)
|
|
|
|
extras = sorted(set(catalog_entries) - assessed_module_ids)
|
|
if extras:
|
|
findings.append(
|
|
Finding(
|
|
"info",
|
|
"catalog_entries_outside_scope",
|
|
"Catalog entries outside this assessment composition: "
|
|
+ ", ".join(extras)
|
|
+ ".",
|
|
)
|
|
)
|
|
|
|
review_targets = conclusions_needing_review(
|
|
assessment=assessment,
|
|
changed_repositories=changed_repositories,
|
|
release_changed=assessed_sequence != catalog_sequence,
|
|
)
|
|
return build_report(
|
|
assessment=assessment,
|
|
catalog=catalog,
|
|
findings=findings,
|
|
changes=changes,
|
|
review_targets=review_targets,
|
|
catalog_checked=True,
|
|
local_tag_provenance_attempted=local_tag_provenance_attempted,
|
|
local_tag_provenance_expected=local_tag_provenance_expected,
|
|
local_tag_catalog_scope_complete=local_tag_catalog_scope_complete,
|
|
)
|
|
|
|
|
|
def validate_assessment(
|
|
*, assessment: dict[str, Any], schema: dict[str, Any]
|
|
) -> list[str]:
|
|
try:
|
|
Draft202012Validator.check_schema(schema)
|
|
except SchemaError as exc:
|
|
return [f"$schema: {exc.message}"]
|
|
validator = Draft202012Validator(schema, format_checker=FormatChecker())
|
|
return [
|
|
f"{_json_path(error.absolute_path)}: {error.message}"
|
|
for error in sorted(
|
|
validator.iter_errors(assessment),
|
|
key=lambda item: (
|
|
tuple(str(part) for part in item.absolute_path),
|
|
str(item.validator),
|
|
item.message,
|
|
),
|
|
)
|
|
]
|
|
|
|
|
|
def validate_catalog(
|
|
*, catalog: dict[str, Any], trusted_keyring: dict[str, Any]
|
|
) -> dict[str, object]:
|
|
trusted_keys = trusted_keys_from_keyring(trusted_keyring)
|
|
channel = _text(catalog.get("channel"))
|
|
with tempfile.NamedTemporaryFile(
|
|
mode="w", suffix=".json", encoding="utf-8"
|
|
) as handle:
|
|
json.dump(catalog, handle)
|
|
handle.flush()
|
|
# Installer replay state answers whether a catalog may be accepted again
|
|
# by one runtime. This read-only assessment instead compares explicit
|
|
# inputs, so ambient installer state must not affect its result.
|
|
with (
|
|
mock.patch.object(
|
|
module_package_catalog,
|
|
"_configured_sequence_state_path",
|
|
return_value=None,
|
|
),
|
|
mock.patch.object(
|
|
module_package_catalog,
|
|
"_configured_enforce_sequence",
|
|
return_value=False,
|
|
),
|
|
):
|
|
return module_package_catalog.validate_module_package_catalog(
|
|
Path(handle.name),
|
|
require_trusted=True,
|
|
approved_channels=(channel,) if channel else (),
|
|
trusted_keys=trusted_keys,
|
|
)
|
|
|
|
|
|
def trusted_keys_from_keyring(payload: dict[str, Any]) -> dict[str, str]:
|
|
now = datetime.now(tz=UTC)
|
|
result: dict[str, str] = {}
|
|
keys = payload.get("keys")
|
|
if not isinstance(keys, list):
|
|
return {
|
|
str(key): str(value)
|
|
for key, value in payload.items()
|
|
if str(key).strip() and str(value).strip()
|
|
}
|
|
for item in keys:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
if str(item.get("status") or "active").strip().lower() in {
|
|
"revoked",
|
|
"disabled",
|
|
"retired",
|
|
}:
|
|
continue
|
|
raw_not_before = item.get("not_before")
|
|
raw_not_after = item.get("not_after")
|
|
not_before = _datetime(raw_not_before)
|
|
not_after = _datetime(raw_not_after)
|
|
if raw_not_before is not None and not_before is None:
|
|
continue
|
|
if raw_not_after is not None and not_after is None:
|
|
continue
|
|
if not_before is not None and now < not_before:
|
|
continue
|
|
if not_after is not None and now > not_after:
|
|
continue
|
|
key_id = _text(item.get("key_id"))
|
|
public_key = _text(item.get("public_key")) or _text(
|
|
item.get("public_key_base64")
|
|
)
|
|
if key_id and public_key:
|
|
result[key_id] = public_key
|
|
return result
|
|
|
|
|
|
def catalog_entries_by_id(
|
|
catalog: dict[str, Any],
|
|
) -> tuple[dict[str, dict[str, Any]], list[Finding]]:
|
|
findings: list[Finding] = []
|
|
entries: dict[str, dict[str, Any]] = {}
|
|
raw_entries: list[tuple[str, dict[str, Any]]] = []
|
|
core = catalog.get("core_release")
|
|
if isinstance(core, dict):
|
|
raw_entries.append(("core", core))
|
|
modules = catalog.get("modules")
|
|
if isinstance(modules, list):
|
|
raw_entries.extend(
|
|
(str(item.get("module_id") or ""), item)
|
|
for item in modules
|
|
if isinstance(item, dict)
|
|
)
|
|
for module_id, item in raw_entries:
|
|
if not module_id:
|
|
findings.append(
|
|
Finding(
|
|
"blocker",
|
|
"catalog_module_id_missing",
|
|
"Catalog entry has no module_id.",
|
|
)
|
|
)
|
|
continue
|
|
if module_id in entries:
|
|
findings.append(
|
|
Finding(
|
|
"blocker",
|
|
"catalog_module_id_duplicate",
|
|
f"Catalog repeats module_id {module_id!r}.",
|
|
)
|
|
)
|
|
continue
|
|
refs = [
|
|
value
|
|
for value in (item.get("python_ref"), item.get("webui_ref"))
|
|
if isinstance(value, str) and value
|
|
]
|
|
entries[module_id] = {
|
|
"module_id": module_id,
|
|
"repository": catalog_repository(item),
|
|
"version": _text(item.get("version")),
|
|
"tags": tuple(
|
|
tag for tag in (catalog_ref_tag(value) for value in refs) if tag
|
|
),
|
|
}
|
|
return entries, findings
|
|
|
|
|
|
def catalog_repository(entry: dict[str, Any]) -> str | None:
|
|
package = _text(entry.get("python_package"))
|
|
if package:
|
|
return package.split("[", 1)[0]
|
|
for field in ("python_ref", "webui_ref"):
|
|
value = _text(entry.get(field))
|
|
if not value:
|
|
continue
|
|
match = re.search(r"/([^/@#]+)[.]git(?:[@#]|$)", value)
|
|
if match:
|
|
return match.group(1)
|
|
return None
|
|
|
|
|
|
def catalog_ref_tag(value: str) -> str | None:
|
|
match = TAG_PATTERN.search(value)
|
|
return match.group("tag") if match else None
|
|
|
|
|
|
def local_tag_provenance(
|
|
*,
|
|
workspace_root: Path,
|
|
repository: str,
|
|
version: str,
|
|
assessed_commit: str,
|
|
selected_commit: str | None = None,
|
|
selected_tag_object: str | None = None,
|
|
) -> dict[str, str] | None:
|
|
if REPOSITORY_ID_PATTERN.fullmatch(repository) is None:
|
|
return {
|
|
"kind": "tag_provenance_unavailable",
|
|
"message": "Cannot verify tag provenance for an invalid repository identifier.",
|
|
}
|
|
resolved_workspace = workspace_root.resolve()
|
|
repo = (resolved_workspace / repository).resolve()
|
|
if repo.parent != resolved_workspace:
|
|
return {
|
|
"kind": "tag_provenance_unavailable",
|
|
"message": f"Cannot verify {repository} tag provenance outside the configured workspace.",
|
|
}
|
|
if not (repo / ".git").exists():
|
|
return {
|
|
"kind": "tag_provenance_unavailable",
|
|
"message": f"Cannot verify {repository} tag provenance because the local checkout is unavailable.",
|
|
}
|
|
try:
|
|
tag_type = subprocess.run(
|
|
["git", "-C", str(repo), "cat-file", "-t", f"refs/tags/v{version}"],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
result = subprocess.run(
|
|
[
|
|
"git",
|
|
"-C",
|
|
str(repo),
|
|
"rev-parse",
|
|
"--verify",
|
|
f"refs/tags/v{version}^{{commit}}",
|
|
],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
tag_object_result = subprocess.run(
|
|
[
|
|
"git",
|
|
"-C",
|
|
str(repo),
|
|
"rev-parse",
|
|
"--verify",
|
|
f"refs/tags/v{version}",
|
|
],
|
|
check=False,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
except OSError:
|
|
return {
|
|
"kind": "tag_provenance_unavailable",
|
|
"message": f"Cannot run the local tag provenance check for {repository}.",
|
|
}
|
|
if tag_type.returncode != 0:
|
|
return {
|
|
"kind": "tag_provenance_unavailable",
|
|
"message": f"Cannot inspect {repository} tag v{version} in the local checkout.",
|
|
}
|
|
if tag_type.stdout.strip() != "tag":
|
|
return {
|
|
"kind": "tag_not_annotated",
|
|
"message": f"{repository} v{version} is not an annotated release tag.",
|
|
}
|
|
if result.returncode != 0:
|
|
return {
|
|
"kind": "tag_provenance_unavailable",
|
|
"message": f"Cannot peel {repository} tag v{version} in the local checkout.",
|
|
}
|
|
if tag_object_result.returncode != 0:
|
|
return {
|
|
"kind": "tag_provenance_unavailable",
|
|
"message": f"Cannot resolve {repository} tag object v{version} in the local checkout.",
|
|
}
|
|
tag_commit = result.stdout.strip().lower()
|
|
tag_object = tag_object_result.stdout.strip().lower()
|
|
if not tag_commit.startswith(assessed_commit.lower()):
|
|
return {
|
|
"kind": "tag_commit_changed",
|
|
"message": f"{repository} v{version} peels to {tag_commit[:12]}, not assessed commit {assessed_commit}.",
|
|
"tag_commit": tag_commit,
|
|
}
|
|
if selected_commit is not None and tag_commit != selected_commit.lower():
|
|
return {
|
|
"kind": "tag_signed_commit_changed",
|
|
"message": (
|
|
f"{repository} v{version} peels to {tag_commit[:12]}, not signed "
|
|
f"selected-unit commit {selected_commit[:12]}."
|
|
),
|
|
"tag_commit": tag_commit,
|
|
"tag_object": tag_object,
|
|
}
|
|
if selected_tag_object is not None and tag_object != selected_tag_object.lower():
|
|
return {
|
|
"kind": "tag_object_changed",
|
|
"message": (
|
|
f"{repository} v{version} resolves to annotated tag object "
|
|
f"{tag_object[:12]}, not signed selected-unit tag object "
|
|
f"{selected_tag_object[:12]}."
|
|
),
|
|
"tag_commit": tag_commit,
|
|
"tag_object": tag_object,
|
|
}
|
|
return None
|
|
|
|
|
|
def conclusions_needing_review(
|
|
*,
|
|
assessment: dict[str, Any],
|
|
changed_repositories: set[str],
|
|
release_changed: bool,
|
|
) -> list[dict[str, Any]]:
|
|
targets: dict[str, dict[str, Any]] = {}
|
|
if release_changed:
|
|
targets["assessment.release"] = {
|
|
"id": "assessment.release",
|
|
"section": "release",
|
|
"reason": "Catalog sequence changed; review release-scoped evidence and conclusions.",
|
|
}
|
|
for section in ("capabilities", "infrastructure"):
|
|
for item in assessment.get(section, []):
|
|
if not isinstance(item, dict):
|
|
continue
|
|
locators = "\n".join(
|
|
str(evidence.get("locator") or "")
|
|
for evidence in item.get("evidence", [])
|
|
if isinstance(evidence, dict)
|
|
).lower()
|
|
matched = sorted(
|
|
repository
|
|
for repository in changed_repositories
|
|
if repository.lower() in locators
|
|
)
|
|
if not matched:
|
|
continue
|
|
item_id = str(item["id"])
|
|
targets[item_id] = {
|
|
"id": item_id,
|
|
"section": section,
|
|
"current_status": item.get("status"),
|
|
"repositories": matched,
|
|
"reason": "Evidence references a repository whose assessed release changed.",
|
|
}
|
|
for module in assessment.get("composition", []):
|
|
if (
|
|
not isinstance(module, dict)
|
|
or module.get("repository") not in changed_repositories
|
|
):
|
|
continue
|
|
module_id = str(module.get("module_id"))
|
|
target_id = f"composition.{module_id}"
|
|
targets[target_id] = {
|
|
"id": target_id,
|
|
"section": "composition",
|
|
"repositories": [module.get("repository")],
|
|
"reason": "Pinned module release or tag provenance changed.",
|
|
}
|
|
return [targets[key] for key in sorted(targets)]
|
|
|
|
|
|
def build_report(
|
|
*,
|
|
assessment: dict[str, Any],
|
|
catalog: dict[str, Any],
|
|
findings: Iterable[Finding],
|
|
changes: list[dict[str, Any]],
|
|
review_targets: list[dict[str, Any]],
|
|
catalog_checked: bool,
|
|
local_tag_provenance_attempted: int,
|
|
local_tag_provenance_expected: int,
|
|
local_tag_catalog_scope_complete: bool,
|
|
) -> dict[str, Any]:
|
|
finding_list = sorted(
|
|
findings,
|
|
key=lambda item: (_severity_rank(item.severity), item.code, item.message),
|
|
)
|
|
finding_codes = {item.code for item in finding_list}
|
|
schema_valid = "assessment_schema" not in finding_codes
|
|
trusted_signature_valid = catalog_checked and "catalog_trust" not in finding_codes
|
|
published_keyring_valid = catalog_checked and not finding_codes.intersection(
|
|
{
|
|
"catalog_keyring_hash_missing",
|
|
"catalog_keyring_hash_mismatch",
|
|
}
|
|
)
|
|
catalog_valid = trusted_signature_valid and published_keyring_valid
|
|
release_valid = catalog_valid and not any(
|
|
item.severity in {"blocker", "review"} for item in finding_list
|
|
)
|
|
tag_provenance_checked = (
|
|
local_tag_provenance_expected > 0
|
|
and local_tag_provenance_attempted == local_tag_provenance_expected
|
|
and local_tag_catalog_scope_complete
|
|
)
|
|
tag_provenance_valid = (
|
|
tag_provenance_checked and "tag_provenance_changed" not in finding_codes
|
|
)
|
|
if any(item.severity == "blocker" for item in finding_list):
|
|
status = "blocked"
|
|
elif any(item.severity == "review" for item in finding_list):
|
|
status = "review_required"
|
|
else:
|
|
status = "current"
|
|
return {
|
|
"report_version": "0.1.0",
|
|
"status": status,
|
|
"assessment_id": assessment.get("assessment_id"),
|
|
"assessment_release": assessment.get("release", {}).get("ref")
|
|
if isinstance(assessment.get("release"), dict)
|
|
else None,
|
|
"catalog": {
|
|
"channel": catalog.get("channel"),
|
|
"sequence": catalog.get("sequence"),
|
|
"generated_at": catalog.get("generated_at"),
|
|
},
|
|
"proof_scope": {
|
|
"assessment_schema": {"checked": True, "valid": schema_valid},
|
|
"catalog_signature_and_keyring": {
|
|
"checked": catalog_checked,
|
|
"valid": catalog_valid if catalog_checked else None,
|
|
},
|
|
"catalog_signature_and_trusted_keyring": {
|
|
"checked": catalog_checked,
|
|
"valid": trusted_signature_valid if catalog_checked else None,
|
|
},
|
|
"published_keyring_hash": {
|
|
"checked": catalog_checked,
|
|
"valid": published_keyring_valid if catalog_checked else None,
|
|
},
|
|
"release_metadata": {
|
|
"checked": catalog_checked,
|
|
"valid": release_valid if catalog_checked else None,
|
|
},
|
|
"local_tag_provenance": {
|
|
"checked": tag_provenance_checked,
|
|
"valid": tag_provenance_valid if tag_provenance_checked else None,
|
|
"attempted_count": local_tag_provenance_attempted,
|
|
"expected_count": local_tag_provenance_expected,
|
|
},
|
|
"installed_artifacts": {"checked": False, "valid": None},
|
|
"target_environment": {"checked": False, "valid": None},
|
|
"external_providers": {"checked": False, "valid": None},
|
|
"production_approval": {"checked": False, "valid": None},
|
|
},
|
|
"findings": [asdict(item) for item in finding_list],
|
|
"changes": sorted(
|
|
changes,
|
|
key=lambda item: (str(item.get("module_id")), str(item.get("kind"))),
|
|
),
|
|
"review_targets": review_targets,
|
|
}
|
|
|
|
|
|
def render_review(report: dict[str, Any]) -> str:
|
|
lines = [
|
|
f"Capability fit rerun: {report['status']}",
|
|
f"Assessment: {report.get('assessment_id') or '-'} ({report.get('assessment_release') or '-'})",
|
|
f"Catalog: {report['catalog'].get('channel') or '-'} sequence {report['catalog'].get('sequence') or '-'}",
|
|
"Scope: schema, signed release metadata, and optional local tag provenance only.",
|
|
]
|
|
findings = report.get("findings") or []
|
|
if findings:
|
|
lines.append("Findings:")
|
|
lines.extend(
|
|
f"- [{item['severity']}] {item['code']}: {item['message']}"
|
|
for item in findings
|
|
)
|
|
else:
|
|
lines.append("Findings: none")
|
|
targets = report.get("review_targets") or []
|
|
if targets:
|
|
lines.append("Conclusions requiring review:")
|
|
lines.extend(f"- {item['id']}: {item['reason']}" for item in targets)
|
|
lines.append(
|
|
"No installed-artifact, target-provider, or production proof was performed."
|
|
)
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def canonical_hash(payload: object) -> str:
|
|
data = json.dumps(
|
|
payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True
|
|
).encode("utf-8")
|
|
return hashlib.sha256(data).hexdigest()
|
|
|
|
|
|
def _catalog_keyring_hash(catalog: dict[str, Any]) -> str | None:
|
|
release = catalog.get("release")
|
|
return _text(release.get("keyring_sha256")) if isinstance(release, dict) else None
|
|
|
|
|
|
def _datetime(value: object) -> datetime | None:
|
|
text = _text(value)
|
|
if text is None:
|
|
return None
|
|
try:
|
|
parsed = datetime.fromisoformat(text.replace("Z", "+00:00"))
|
|
except ValueError:
|
|
return None
|
|
return parsed if parsed.tzinfo is not None else parsed.replace(tzinfo=UTC)
|
|
|
|
|
|
def _integer(value: object) -> int | None:
|
|
try:
|
|
return int(value) if value is not None else None
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def _json_path(parts: Iterable[object]) -> str:
|
|
path = "$"
|
|
for part in parts:
|
|
path += f"[{part}]" if isinstance(part, int) else f".{part}"
|
|
return path
|
|
|
|
|
|
def _severity_rank(severity: str) -> int:
|
|
return {"blocker": 0, "review": 1, "info": 2}.get(severity, 3)
|
|
|
|
|
|
def _text(value: object) -> str | None:
|
|
return value if isinstance(value, str) and value else None
|