feat(assessment): gate signed release drift
This commit is contained in:
694
tools/assessments/govoplan_assessment/capability_fit.py
Normal file
694
tools/assessments/govoplan_assessment/capability_fit.py
Normal file
@@ -0,0 +1,694 @@
|
||||
"""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 jsonschema import Draft202012Validator, FormatChecker
|
||||
from jsonschema.exceptions import SchemaError
|
||||
|
||||
from govoplan_core.core.module_package_catalog import validate_module_package_catalog
|
||||
|
||||
|
||||
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],
|
||||
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_checked=False,
|
||||
)
|
||||
|
||||
catalog_validation = validate_catalog(catalog=catalog, keyring=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",),
|
||||
)
|
||||
)
|
||||
expected_keyring_hash = _catalog_keyring_hash(catalog)
|
||||
actual_keyring_hash = canonical_hash(keyring)
|
||||
if expected_keyring_hash is None:
|
||||
findings.append(
|
||||
Finding(
|
||||
"blocker",
|
||||
"catalog_keyring_hash_missing",
|
||||
"Catalog release metadata does not pin the trusted keyring hash.",
|
||||
("assessment.release",),
|
||||
)
|
||||
)
|
||||
elif expected_keyring_hash != actual_keyring_hash:
|
||||
findings.append(
|
||||
Finding(
|
||||
"blocker",
|
||||
"catalog_keyring_hash_mismatch",
|
||||
"The supplied 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()
|
||||
|
||||
for module in assessment["composition"]:
|
||||
module_id = str(module["module_id"])
|
||||
repository = str(module["repository"])
|
||||
assessed_module_ids.add(module_id)
|
||||
entry = catalog_entries.get(module_id)
|
||||
if entry is None:
|
||||
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}",),
|
||||
)
|
||||
)
|
||||
|
||||
assessed_version = str(module["manifest_version"])
|
||||
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}",),
|
||||
)
|
||||
)
|
||||
|
||||
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}",),
|
||||
)
|
||||
)
|
||||
|
||||
if workspace_root is not None:
|
||||
provenance = local_tag_provenance(
|
||||
workspace_root=workspace_root,
|
||||
repository=repository,
|
||||
version=assessed_version,
|
||||
assessed_commit=str(module["commit"]),
|
||||
)
|
||||
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"),
|
||||
}
|
||||
)
|
||||
changed_repositories.add(repository)
|
||||
findings.append(
|
||||
Finding(
|
||||
"review",
|
||||
"tag_provenance_changed",
|
||||
str(provenance["message"]),
|
||||
(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_checked=workspace_root is not None,
|
||||
)
|
||||
|
||||
|
||||
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], keyring: dict[str, Any]
|
||||
) -> dict[str, object]:
|
||||
trusted_keys = trusted_keys_from_keyring(keyring)
|
||||
channel = _text(catalog.get("channel"))
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".json", encoding="utf-8"
|
||||
) as handle:
|
||||
json.dump(catalog, handle)
|
||||
handle.flush()
|
||||
return 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 result
|
||||
for item in keys:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if str(item.get("status") or "active").strip().lower() != "active":
|
||||
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,
|
||||
) -> 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,
|
||||
)
|
||||
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.",
|
||||
}
|
||||
tag_commit = 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,
|
||||
}
|
||||
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_checked: 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
|
||||
catalog_valid = catalog_checked and not finding_codes.intersection(
|
||||
{
|
||||
"catalog_trust",
|
||||
"catalog_keyring_hash_missing",
|
||||
"catalog_keyring_hash_mismatch",
|
||||
}
|
||||
)
|
||||
release_valid = catalog_valid and not any(
|
||||
item.severity in {"blocker", "review"} for item in finding_list
|
||||
)
|
||||
tag_provenance_valid = (
|
||||
local_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,
|
||||
},
|
||||
"release_metadata": {
|
||||
"checked": catalog_checked,
|
||||
"valid": release_valid if catalog_checked else None,
|
||||
},
|
||||
"local_tag_provenance": {
|
||||
"checked": local_tag_provenance_checked,
|
||||
"valid": tag_provenance_valid if local_tag_provenance_checked else None,
|
||||
},
|
||||
"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
|
||||
Reference in New Issue
Block a user