fix(assessment): harden capability evidence trust
This commit is contained in:
@@ -7,6 +7,7 @@ import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
@@ -22,6 +23,10 @@ from govoplan_assessment import ( # noqa: E402
|
||||
render_review,
|
||||
review_capability_fit,
|
||||
)
|
||||
from govoplan_assessment.atomic_io import ( # noqa: E402
|
||||
AtomicJsonWriteError,
|
||||
atomic_write_json,
|
||||
)
|
||||
from govoplan_assessment.evidence import validate_payload # noqa: E402
|
||||
from govoplan_release.catalog import DEFAULT_PUBLIC_BASE_URL, fetch_json # noqa: E402
|
||||
|
||||
@@ -29,6 +34,8 @@ from govoplan_release.catalog import DEFAULT_PUBLIC_BASE_URL, fetch_json # noqa
|
||||
TRUSTED_KEYRING_FILE_ENV = "GOVOPLAN_MODULE_PACKAGE_CATALOG_TRUSTED_KEYS_FILE"
|
||||
MAX_ASSESSMENT_INPUT_BYTES = 16 * 1024 * 1024
|
||||
MAX_EVIDENCE_INPUT_BYTES = 4 * 1024 * 1024
|
||||
MAX_REPORT_OUTPUT_BYTES = 16 * 1024 * 1024
|
||||
OPAQUE_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$")
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
@@ -130,6 +137,13 @@ def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
"--verification-time",
|
||||
help="Optional ISO-8601 time for reproducible boundary-proof verification.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--expected-external-provider-subject",
|
||||
help=(
|
||||
"Opaque provider-environment ID that external-provider proof must match; "
|
||||
"never supply a URL, credential, or endpoint identifier."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json", action="store_true", help="Print the machine-readable review report."
|
||||
)
|
||||
@@ -157,6 +171,13 @@ def main(argv: list[str] | None = None) -> int:
|
||||
raise SystemExit(
|
||||
"--boundary-evidence requires --installed-evidence or --collect-installed-evidence"
|
||||
)
|
||||
if (
|
||||
args.expected_external_provider_subject is not None
|
||||
and OPAQUE_ID_PATTERN.fullmatch(args.expected_external_provider_subject) is None
|
||||
):
|
||||
raise SystemExit(
|
||||
"--expected-external-provider-subject must be a bounded opaque ID"
|
||||
)
|
||||
if (
|
||||
args.collect_installed_evidence is not None
|
||||
and args.output is not None
|
||||
@@ -232,7 +253,12 @@ def main(argv: list[str] | None = None) -> int:
|
||||
raise SystemExit(
|
||||
"Collected installed evidence did not satisfy its bounded schema"
|
||||
)
|
||||
write_object(args.collect_installed_evidence, installed_evidence)
|
||||
write_object(
|
||||
args.collect_installed_evidence,
|
||||
installed_evidence,
|
||||
max_bytes=MAX_EVIDENCE_INPUT_BYTES,
|
||||
label="installed evidence",
|
||||
)
|
||||
|
||||
boundary_evidence = None
|
||||
boundary_evidence_schema = None
|
||||
@@ -260,6 +286,7 @@ def main(argv: list[str] | None = None) -> int:
|
||||
max_bytes=MAX_EVIDENCE_INPUT_BYTES,
|
||||
)
|
||||
|
||||
verification_time = parse_datetime(args.verification_time)
|
||||
report = review_capability_fit(
|
||||
assessment=assessment,
|
||||
schema=schema,
|
||||
@@ -275,12 +302,22 @@ def main(argv: list[str] | None = None) -> int:
|
||||
boundary_evidence_schema=boundary_evidence_schema,
|
||||
boundary_authority_keyring=boundary_authority_keyring,
|
||||
boundary_authority_keyring_schema=boundary_authority_keyring_schema,
|
||||
verification_time=parse_datetime(args.verification_time),
|
||||
verification_time=verification_time,
|
||||
installed_evidence_mode=(
|
||||
"direct_local"
|
||||
if args.collect_installed_evidence is not None
|
||||
else "imported_unsigned"
|
||||
),
|
||||
expected_external_provider_subject=args.expected_external_provider_subject,
|
||||
)
|
||||
encoded = json.dumps(report, indent=2, sort_keys=True) + "\n"
|
||||
if args.output is not None:
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(encoded, encoding="utf-8")
|
||||
write_object(
|
||||
args.output,
|
||||
report,
|
||||
max_bytes=MAX_REPORT_OUTPUT_BYTES,
|
||||
label="review report",
|
||||
)
|
||||
sys.stdout.write(encoded if args.json else render_review(report))
|
||||
return {"current": 0, "blocked": 1, "review_required": 2}[report["status"]]
|
||||
|
||||
@@ -307,16 +344,19 @@ def read_object(path: Path | None, *, label: str, max_bytes: int) -> dict[str, o
|
||||
return payload
|
||||
|
||||
|
||||
def write_object(path: Path | None, payload: dict[str, object]) -> None:
|
||||
def write_object(
|
||||
path: Path | None,
|
||||
payload: dict[str, object],
|
||||
*,
|
||||
max_bytes: int,
|
||||
label: str,
|
||||
) -> None:
|
||||
if path is None:
|
||||
raise SystemExit("Missing installed evidence output path")
|
||||
encoded = json.dumps(payload, indent=2, sort_keys=True) + "\n"
|
||||
if len(encoded.encode("utf-8")) > MAX_EVIDENCE_INPUT_BYTES:
|
||||
raise SystemExit(
|
||||
"Collected installed evidence exceeds the bounded output limit"
|
||||
)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(encoded, encoding="utf-8")
|
||||
raise SystemExit(f"Missing {label} output path")
|
||||
try:
|
||||
atomic_write_json(path, payload, max_bytes=max_bytes)
|
||||
except AtomicJsonWriteError as exc:
|
||||
raise SystemExit(f"Could not securely write {label}") from exc
|
||||
|
||||
|
||||
def parse_datetime(value: str | None):
|
||||
|
||||
Reference in New Issue
Block a user