Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a0291a-1e87-7151-9934-fcbfe3f65eb1
766 lines
28 KiB
Python
766 lines
28 KiB
Python
#!/usr/bin/env python3
|
|
"""Resolve security-zone declarations without guessing workload identity."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import sys
|
|
from datetime import date
|
|
from pathlib import Path
|
|
from typing import Any, Iterable, Mapping
|
|
|
|
import yaml
|
|
|
|
|
|
STANDARD = "security-zones_v0.1"
|
|
INPUT_STANDARD = "zone-resolver-input_v0.1"
|
|
PROFILE_STANDARD = "security-zone-control-profile_v0.1"
|
|
MATURITY_RANK = {"M0": 0, "M1": 1, "M2": 2, "M3": 3}
|
|
CRITICALITY_FLOOR = {"low": 0, "medium": 1, "high": 2, "critical": 3}
|
|
DATACLASS_FLOOR = {
|
|
"synthetic": 0,
|
|
"internal": 1,
|
|
"confidential": 2,
|
|
"restricted": 3,
|
|
}
|
|
ZONE_FLOOR = {
|
|
"z0-experimental": 0,
|
|
"z1-operational": 1,
|
|
"z2-protected": 2,
|
|
"z2-continuity": 2,
|
|
"z3-critical": 3,
|
|
}
|
|
PROFILE_ZONES = frozenset({*ZONE_FLOOR, "unknown"})
|
|
|
|
|
|
class DeclarationError(ValueError):
|
|
"""A declaration violates the security-zones_v0.1 contract."""
|
|
|
|
|
|
class ProfileError(ValueError):
|
|
"""A control profile lacks authoritative, total provenance."""
|
|
|
|
|
|
def _required(mapping: Mapping[str, Any], key: str, where: str) -> Any:
|
|
value = mapping.get(key)
|
|
if value is None or value == "" or value == []:
|
|
raise DeclarationError(f"{where}.{key} is required")
|
|
return value
|
|
|
|
|
|
def _canonical(value: Any) -> Any:
|
|
"""Return a stable, mapping- and list-order-independent JSON value."""
|
|
|
|
if isinstance(value, Mapping):
|
|
return {str(key): _canonical(value[key]) for key in sorted(value)}
|
|
if isinstance(value, list):
|
|
items = [_canonical(item) for item in value]
|
|
return sorted(
|
|
items,
|
|
key=lambda item: json.dumps(
|
|
item, sort_keys=True, separators=(",", ":"), default=str
|
|
),
|
|
)
|
|
if isinstance(value, date):
|
|
return value.isoformat()
|
|
return value
|
|
|
|
|
|
def _digest(value: Any) -> str:
|
|
encoded = json.dumps(
|
|
_canonical(value), sort_keys=True, separators=(",", ":"), default=str
|
|
).encode()
|
|
return "sha256:" + hashlib.sha256(encoded).hexdigest()
|
|
|
|
|
|
def _parse_date(value: Any, where: str) -> date:
|
|
if isinstance(value, date):
|
|
return value
|
|
try:
|
|
return date.fromisoformat(str(value))
|
|
except ValueError as exc:
|
|
raise DeclarationError(f"{where} must be an ISO date") from exc
|
|
|
|
|
|
def _services(document: Mapping[str, Any]) -> Iterable[dict[str, Any]]:
|
|
services = document.get("services")
|
|
if services is not None:
|
|
if not isinstance(services, list) or not services:
|
|
raise DeclarationError("services must be a non-empty list")
|
|
if "zones" in document or "workload_identity" in document:
|
|
raise DeclarationError(
|
|
"multi-service declarations keep zones and workload_identity per service"
|
|
)
|
|
yield from services
|
|
return
|
|
yield dict(document)
|
|
|
|
|
|
def _validate_identity(service: str, identity: Any) -> dict[str, Any]:
|
|
if not isinstance(identity, dict):
|
|
raise DeclarationError(f"{service}.workload_identity must be a mapping")
|
|
name = str(_required(identity, "name", f"{service}.workload_identity"))
|
|
if name != service:
|
|
raise DeclarationError(
|
|
f"{service}.workload_identity.name must equal service, got {name!r}"
|
|
)
|
|
_required(identity, "kind", f"{service}.workload_identity")
|
|
_required(identity, "responsible_repo", f"{service}.workload_identity")
|
|
bindings = _required(
|
|
identity, "identity_bindings", f"{service}.workload_identity"
|
|
)
|
|
if not isinstance(bindings, list) or not bindings:
|
|
raise DeclarationError(
|
|
f"{service}.workload_identity.identity_bindings must be a non-empty list"
|
|
)
|
|
seen: set[tuple[str, str, str, str]] = set()
|
|
for index, binding in enumerate(bindings):
|
|
where = f"{service}.workload_identity.identity_bindings[{index}]"
|
|
if not isinstance(binding, dict):
|
|
raise DeclarationError(f"{where} must be a mapping")
|
|
for key in ("scheme", "authority", "subject", "principal_type"):
|
|
_required(binding, key, where)
|
|
if binding["principal_type"] not in {"service", "agent"}:
|
|
raise DeclarationError(f"{where}.principal_type must be service or agent")
|
|
identity_key = tuple(
|
|
str(binding[key])
|
|
for key in ("scheme", "authority", "subject", "principal_type")
|
|
)
|
|
if identity_key in seen:
|
|
raise DeclarationError(f"{where} duplicates an identity binding")
|
|
seen.add(identity_key)
|
|
return identity
|
|
|
|
|
|
def _binding_refs(identity: Mapping[str, Any] | None) -> list[str]:
|
|
if not identity:
|
|
return []
|
|
return sorted(
|
|
"/".join(
|
|
str(binding[key]) for key in ("scheme", "authority", "subject")
|
|
)
|
|
for binding in identity["identity_bindings"]
|
|
)
|
|
|
|
|
|
def _validate_workload_ref(
|
|
service: str,
|
|
workload_ref: Any,
|
|
identity: Mapping[str, Any] | None,
|
|
) -> dict[str, Any]:
|
|
if workload_ref is None:
|
|
if identity is None:
|
|
return {
|
|
"applicability": "applicable",
|
|
"rapp_id": None,
|
|
"name": None,
|
|
"deployable": None,
|
|
}
|
|
return {
|
|
"applicability": "applicable",
|
|
"rapp_id": None,
|
|
"name": str(identity["name"]),
|
|
"deployable": None,
|
|
}
|
|
if not isinstance(workload_ref, Mapping):
|
|
raise DeclarationError(f"{service}.workload_ref must be a mapping")
|
|
applicability = str(
|
|
_required(workload_ref, "applicability", f"{service}.workload_ref")
|
|
)
|
|
if applicability not in {"applicable", "not-applicable"}:
|
|
raise DeclarationError(
|
|
f"{service}.workload_ref.applicability must be applicable or not-applicable"
|
|
)
|
|
rapp_id = workload_ref.get("rapp_id") or None
|
|
name = workload_ref.get("name") or None
|
|
deployable = workload_ref.get("deployable") or None
|
|
if applicability == "not-applicable":
|
|
if any(value is not None for value in (rapp_id, name, deployable)):
|
|
raise DeclarationError(
|
|
f"{service}.workload_ref not-applicable must not carry a workload tuple"
|
|
)
|
|
if identity is not None:
|
|
raise DeclarationError(
|
|
f"{service} cannot be both an authoritative workload and not-applicable"
|
|
)
|
|
return {
|
|
"applicability": applicability,
|
|
"rapp_id": None,
|
|
"name": None,
|
|
"deployable": None,
|
|
}
|
|
if deployable is not None and rapp_id is None:
|
|
raise DeclarationError(
|
|
f"{service}.workload_ref.deployable requires rapp_id"
|
|
)
|
|
if name is not None:
|
|
name = str(name)
|
|
if identity is not None and name is not None and name != identity["name"]:
|
|
raise DeclarationError(
|
|
f"{service}.workload_ref.name must equal workload_identity.name"
|
|
)
|
|
return {
|
|
"applicability": applicability,
|
|
"rapp_id": str(rapp_id) if rapp_id is not None else None,
|
|
"name": name,
|
|
"deployable": str(deployable) if deployable is not None else None,
|
|
}
|
|
|
|
|
|
def _admission(service: str, zones: Mapping[str, Any]) -> tuple[str, str]:
|
|
membership = str(_required(zones, "membership", f"{service}.zones"))
|
|
if membership not in ZONE_FLOOR:
|
|
raise DeclarationError(f"{service}.zones.membership is unknown: {membership!r}")
|
|
context = _required(zones, "context", f"{service}.zones")
|
|
if not isinstance(context, Mapping):
|
|
raise DeclarationError(f"{service}.zones.context must be a mapping")
|
|
maturity = str(_required(context, "maturity", f"{service}.zones.context"))
|
|
criticality = str(
|
|
_required(context, "criticality", f"{service}.zones.context")
|
|
)
|
|
dataclass = str(
|
|
_required(context, "data_classification", f"{service}.zones.context")
|
|
)
|
|
if maturity not in MATURITY_RANK:
|
|
raise DeclarationError(f"{service}.zones.context.maturity is invalid")
|
|
if criticality not in CRITICALITY_FLOOR:
|
|
raise DeclarationError(f"{service}.zones.context.criticality is invalid")
|
|
if dataclass == "public":
|
|
return "unknown", "public_data_classification_floor_unresolved"
|
|
if dataclass == "n/a":
|
|
_required(context, "data_classification_reason", f"{service}.zones.context")
|
|
data_floor = 0
|
|
elif dataclass in DATACLASS_FLOOR:
|
|
data_floor = DATACLASS_FLOOR[dataclass]
|
|
else:
|
|
raise DeclarationError(
|
|
f"{service}.zones.context.data_classification is invalid"
|
|
)
|
|
zone_rank = ZONE_FLOOR[membership]
|
|
context_rank = max(CRITICALITY_FLOOR[criticality], data_floor)
|
|
if zone_rank < context_rank:
|
|
return "unsatisfied", f"{membership}_below_M{context_rank}_context_floor"
|
|
if MATURITY_RANK[maturity] < zone_rank:
|
|
return "unsatisfied", f"{maturity}_below_M{zone_rank}_zone_floor"
|
|
if membership == "z2-continuity":
|
|
supported = {
|
|
str(fact)
|
|
for item in zones["evidence"]
|
|
if isinstance(item, Mapping)
|
|
for fact in item.get("supports", [])
|
|
}
|
|
required = {"continuity-dependency", "recovery"}
|
|
if not required.issubset(supported):
|
|
return "unsatisfied", "continuity_evidence_incomplete"
|
|
return "satisfied", "admission_floor_met"
|
|
|
|
|
|
def _base_record(
|
|
service: str,
|
|
source: str,
|
|
source_revision: str | None,
|
|
workload_ref: Mapping[str, Any],
|
|
identity: Mapping[str, Any] | None,
|
|
) -> dict[str, Any]:
|
|
return {
|
|
"subject_id": service,
|
|
"workload_id": workload_ref.get("name"),
|
|
"workload_ref": dict(workload_ref),
|
|
"identity_bindings": _binding_refs(identity),
|
|
"declared_zone": None,
|
|
"admission": "unknown",
|
|
"admission_reason": "zone_membership_absent",
|
|
"effective_zone": "unknown",
|
|
"membership_revision": None,
|
|
"membership_revision_reason": "source_revision_absent"
|
|
if source_revision is None
|
|
else "zone_membership_absent",
|
|
"guarantees": ["non-inferred-resolution"],
|
|
"source": source,
|
|
"source_revision": source_revision,
|
|
}
|
|
|
|
|
|
def resolve_service(
|
|
service_entry: dict[str, Any],
|
|
source: str,
|
|
*,
|
|
source_revision: str | None = None,
|
|
workload_ref: Mapping[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
service = str(_required(service_entry, "service", source))
|
|
identity_value = service_entry.get("workload_identity")
|
|
identity = (
|
|
_validate_identity(service, identity_value)
|
|
if identity_value is not None
|
|
else None
|
|
)
|
|
reference = _validate_workload_ref(service, workload_ref, identity)
|
|
record = _base_record(service, source, source_revision, reference, identity)
|
|
|
|
if reference["applicability"] == "not-applicable":
|
|
record.update(
|
|
{
|
|
"workload_id": None,
|
|
"admission": "not-applicable",
|
|
"admission_reason": "catalog_declared_not_applicable",
|
|
"effective_zone": None,
|
|
"membership_revision_reason": "not_applicable",
|
|
"guarantees": [
|
|
"catalog-declared-not-applicable",
|
|
"non-inferred-resolution",
|
|
],
|
|
}
|
|
)
|
|
return record
|
|
|
|
if reference["name"] is None:
|
|
record["admission_reason"] = "workload_reference_unresolved"
|
|
record["membership_revision_reason"] = "workload_reference_unresolved"
|
|
return record
|
|
|
|
record["workload_id"] = reference["name"]
|
|
if identity is None:
|
|
if service_entry.get("zones") is not None:
|
|
raise DeclarationError(
|
|
f"{service}.workload_identity is required beside zones"
|
|
)
|
|
record["admission_reason"] = "workload_identity_unresolved"
|
|
record["membership_revision_reason"] = "workload_identity_unresolved"
|
|
record["guarantees"].append("explicit-workload-reference")
|
|
return record
|
|
|
|
record["guarantees"].extend(
|
|
["authoritative-workload-identity", "explicit-workload-reference"]
|
|
)
|
|
zones = service_entry.get("zones")
|
|
if zones is None:
|
|
return record
|
|
if not isinstance(zones, dict):
|
|
raise DeclarationError(f"{service}.zones must be a mapping")
|
|
if zones.get("standard") != STANDARD:
|
|
raise DeclarationError(f"{service}.zones.standard must be {STANDARD}")
|
|
for key in (
|
|
"responsible_party",
|
|
"justification",
|
|
"evidence",
|
|
"reviewed",
|
|
"review_due",
|
|
):
|
|
_required(zones, key, f"{service}.zones")
|
|
if not isinstance(zones["evidence"], list) or not zones["evidence"]:
|
|
raise DeclarationError(f"{service}.zones.evidence must be a non-empty list")
|
|
reviewed = _parse_date(zones["reviewed"], f"{service}.zones.reviewed")
|
|
review_due = _parse_date(zones["review_due"], f"{service}.zones.review_due")
|
|
if review_due <= reviewed:
|
|
raise DeclarationError(f"{service}.zones.review_due must be after reviewed")
|
|
admission, reason = _admission(service, zones)
|
|
membership = str(zones["membership"])
|
|
effective = membership if admission == "satisfied" else "unknown"
|
|
revision = None
|
|
revision_reason = "source_revision_absent"
|
|
if source_revision:
|
|
revision = _digest(
|
|
{
|
|
"source_revision": source_revision,
|
|
"workload_ref": reference,
|
|
"workload_identity": identity,
|
|
"zones": zones,
|
|
}
|
|
)
|
|
revision_reason = "source_bound"
|
|
record.update(
|
|
{
|
|
"declared_zone": membership,
|
|
"admission": admission,
|
|
"admission_reason": reason,
|
|
"effective_zone": effective,
|
|
"membership_revision": revision,
|
|
"membership_revision_reason": revision_reason,
|
|
"guarantees": sorted(
|
|
set(
|
|
record["guarantees"]
|
|
+ ["explicit-zone-membership"]
|
|
+ (["source-revision-bound-membership"] if revision else [])
|
|
)
|
|
),
|
|
}
|
|
)
|
|
return record
|
|
|
|
|
|
def validate_control_profile(profile: Any) -> dict[str, Any]:
|
|
if not isinstance(profile, Mapping):
|
|
raise ProfileError("control profile must be a mapping")
|
|
if profile.get("standard") != PROFILE_STANDARD:
|
|
raise ProfileError(f"control profile standard must be {PROFILE_STANDARD}")
|
|
profile_id = profile.get("profile_id")
|
|
version = profile.get("version")
|
|
if not profile_id or not version:
|
|
raise ProfileError("control profile requires profile_id and version")
|
|
controls = profile.get("controls")
|
|
if not isinstance(controls, Mapping) or not controls:
|
|
raise ProfileError("control profile controls must be a non-empty mapping")
|
|
normalized: dict[str, Any] = {
|
|
"standard": PROFILE_STANDARD,
|
|
"profile_id": str(profile_id),
|
|
"version": str(version),
|
|
"controls": {},
|
|
}
|
|
for control_id in sorted(controls):
|
|
definition = controls[control_id]
|
|
where = f"controls.{control_id}"
|
|
if not isinstance(definition, Mapping):
|
|
raise ProfileError(f"{where} must be a mapping")
|
|
policy_owner = definition.get("policy_owner")
|
|
pep_owner = definition.get("pep_owner")
|
|
policy_ref = definition.get("policy_ref")
|
|
if not policy_owner or not pep_owner or not policy_ref:
|
|
raise ProfileError(
|
|
f"{where} requires policy_owner, pep_owner, and policy_ref"
|
|
)
|
|
if "/" not in str(control_id) or not str(control_id).startswith(
|
|
f"{policy_owner}/"
|
|
):
|
|
raise ProfileError(
|
|
f"{where} id must be owner-qualified by policy_owner"
|
|
)
|
|
mappings = definition.get("zones")
|
|
if not isinstance(mappings, Mapping):
|
|
raise ProfileError(f"{where}.zones must be a mapping")
|
|
supplied = set(mappings)
|
|
if supplied != PROFILE_ZONES:
|
|
missing = sorted(PROFILE_ZONES - supplied)
|
|
extra = sorted(supplied - PROFILE_ZONES)
|
|
raise ProfileError(
|
|
f"{where}.zones must be total; missing={missing}, extra={extra}"
|
|
)
|
|
normalized_zones: dict[str, dict[str, Any]] = {}
|
|
for zone in sorted(PROFILE_ZONES):
|
|
rule = mappings[zone]
|
|
if not isinstance(rule, Mapping):
|
|
raise ProfileError(f"{where}.zones.{zone} must be a mapping")
|
|
stance = rule.get("stance")
|
|
failure_mode = rule.get("failure_mode")
|
|
if stance not in {"enforced", "advisory", "exempt"}:
|
|
raise ProfileError(f"{where}.zones.{zone}.stance is invalid")
|
|
if stance == "exempt":
|
|
if failure_mode not in {None, ""}:
|
|
raise ProfileError(
|
|
f"{where}.zones.{zone} exempt must not have failure_mode"
|
|
)
|
|
failure_mode = None
|
|
elif failure_mode not in {"fail_open", "fail_closed"}:
|
|
raise ProfileError(
|
|
f"{where}.zones.{zone}.failure_mode is invalid"
|
|
)
|
|
normalized_zones[zone] = {
|
|
"stance": stance,
|
|
"failure_mode": failure_mode,
|
|
}
|
|
normalized["controls"][str(control_id)] = {
|
|
"policy_owner": str(policy_owner),
|
|
"pep_owner": str(pep_owner),
|
|
"policy_ref": str(policy_ref),
|
|
"zones": normalized_zones,
|
|
}
|
|
return normalized
|
|
|
|
|
|
def project_controls(record: dict[str, Any], profile: Mapping[str, Any]) -> None:
|
|
zone = record.get("effective_zone")
|
|
if zone not in PROFILE_ZONES:
|
|
return
|
|
record["control_profile"] = {
|
|
"id": profile["profile_id"],
|
|
"version": profile["version"],
|
|
}
|
|
record["controls"] = []
|
|
for control_id, definition in profile["controls"].items():
|
|
rule = definition["zones"][zone]
|
|
record["controls"].append(
|
|
{
|
|
"id": control_id,
|
|
"policy_owner": definition["policy_owner"],
|
|
"pep_owner": definition["pep_owner"],
|
|
"policy_ref": definition["policy_ref"],
|
|
"stance": rule["stance"],
|
|
"failure_mode": rule["failure_mode"],
|
|
}
|
|
)
|
|
|
|
|
|
def compare_snapshots(
|
|
records: Iterable[Mapping[str, Any]],
|
|
previous: Mapping[str, Any] | None,
|
|
) -> dict[str, Any]:
|
|
current_by_id = {str(record["subject_id"]): record for record in records}
|
|
previous_records = previous.get("records", []) if previous else []
|
|
previous_by_id = {
|
|
str(record["subject_id"]): record
|
|
for record in previous_records
|
|
if isinstance(record, Mapping) and record.get("subject_id")
|
|
}
|
|
current_ids = set(current_by_id)
|
|
previous_ids = set(previous_by_id)
|
|
changed: list[dict[str, Any]] = []
|
|
for subject_id in sorted(current_ids & previous_ids):
|
|
current = current_by_id[subject_id]
|
|
prior = previous_by_id[subject_id]
|
|
fields = (
|
|
"workload_ref",
|
|
"identity_bindings",
|
|
"declared_zone",
|
|
"admission",
|
|
"effective_zone",
|
|
"membership_revision",
|
|
)
|
|
if any(_canonical(current.get(key)) != _canonical(prior.get(key)) for key in fields):
|
|
changed.append(
|
|
{
|
|
"subject_id": subject_id,
|
|
"before_revision": prior.get("membership_revision"),
|
|
"after_revision": current.get("membership_revision"),
|
|
}
|
|
)
|
|
return {
|
|
"baseline": "previous" if previous is not None else "initial",
|
|
"added": sorted(current_ids - previous_ids),
|
|
"removed": sorted(previous_ids - current_ids),
|
|
"changed": changed,
|
|
}
|
|
|
|
|
|
def _resolve_documents(
|
|
sources: Iterable[tuple[Path, str | None, Mapping[str, Any]]],
|
|
*,
|
|
profile: Mapping[str, Any] | None = None,
|
|
previous: Mapping[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
records: list[dict[str, Any]] = []
|
|
errors: list[dict[str, str]] = []
|
|
profile_errors: list[str] = []
|
|
validated_profile: Mapping[str, Any] | None = None
|
|
if profile is not None:
|
|
try:
|
|
validated_profile = validate_control_profile(profile)
|
|
except ProfileError as exc:
|
|
profile_errors.append(str(exc))
|
|
for path, source_revision, workload_refs in sources:
|
|
try:
|
|
document = yaml.safe_load(path.read_text()) or {}
|
|
if not isinstance(document, Mapping):
|
|
raise DeclarationError("document must be a mapping")
|
|
for entry in _services(document):
|
|
if not isinstance(entry, dict):
|
|
raise DeclarationError("service entry must be a mapping")
|
|
service = str(_required(entry, "service", str(path)))
|
|
record = resolve_service(
|
|
entry,
|
|
str(path),
|
|
source_revision=source_revision,
|
|
workload_ref=workload_refs.get(service),
|
|
)
|
|
if validated_profile is not None:
|
|
project_controls(record, validated_profile)
|
|
records.append(record)
|
|
except (OSError, yaml.YAMLError, DeclarationError) as exc:
|
|
errors.append({"source": str(path), "error": str(exc)})
|
|
records.sort(key=lambda record: str(record["subject_id"]))
|
|
duplicate_ids = sorted(
|
|
subject_id
|
|
for subject_id in {record["subject_id"] for record in records}
|
|
if sum(record["subject_id"] == subject_id for record in records) > 1
|
|
)
|
|
if duplicate_ids:
|
|
errors.append(
|
|
{
|
|
"source": "resolved-records",
|
|
"error": f"duplicate subject ids: {duplicate_ids}",
|
|
}
|
|
)
|
|
return {
|
|
"ok": not errors and not profile_errors,
|
|
"standard": STANDARD,
|
|
"records": records,
|
|
"changes": compare_snapshots(records, previous),
|
|
"errors": errors,
|
|
"profile_errors": profile_errors,
|
|
}
|
|
|
|
|
|
def resolve_paths(
|
|
paths: Iterable[Path],
|
|
*,
|
|
source_revision: str | None = None,
|
|
source_revisions: Mapping[str, str] | None = None,
|
|
workload_refs: Mapping[str, Any] | None = None,
|
|
profile: Mapping[str, Any] | None = None,
|
|
previous: Mapping[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
revisions = source_revisions or {}
|
|
refs = workload_refs or {}
|
|
sources = [
|
|
(
|
|
path,
|
|
revisions.get(str(path), source_revision),
|
|
refs,
|
|
)
|
|
for path in paths
|
|
]
|
|
return _resolve_documents(sources, profile=profile, previous=previous)
|
|
|
|
|
|
def resolve_manifest(
|
|
manifest: Mapping[str, Any],
|
|
*,
|
|
base_dir: Path,
|
|
profile: Mapping[str, Any] | None = None,
|
|
previous: Mapping[str, Any] | None = None,
|
|
) -> dict[str, Any]:
|
|
if manifest.get("standard") != INPUT_STANDARD:
|
|
raise DeclarationError(f"manifest.standard must be {INPUT_STANDARD}")
|
|
entries = manifest.get("sources")
|
|
if not isinstance(entries, list) or not entries:
|
|
raise DeclarationError("manifest.sources must be a non-empty list")
|
|
sources: list[tuple[Path, str | None, Mapping[str, Any]]] = []
|
|
for index, entry in enumerate(entries):
|
|
if not isinstance(entry, Mapping):
|
|
raise DeclarationError(f"manifest.sources[{index}] must be a mapping")
|
|
path_value = _required(entry, "path", f"manifest.sources[{index}]")
|
|
path = Path(str(path_value))
|
|
if not path.is_absolute():
|
|
path = base_dir / path
|
|
source_revision = entry.get("source_revision")
|
|
refs = entry.get("workload_refs") or {}
|
|
if not isinstance(refs, Mapping):
|
|
raise DeclarationError(
|
|
f"manifest.sources[{index}].workload_refs must be a mapping"
|
|
)
|
|
sources.append(
|
|
(
|
|
path,
|
|
str(source_revision) if source_revision else None,
|
|
refs,
|
|
)
|
|
)
|
|
result = _resolve_documents(sources, profile=profile, previous=previous)
|
|
subjects = manifest.get("subjects") or []
|
|
if not isinstance(subjects, list):
|
|
raise DeclarationError("manifest.subjects must be a list")
|
|
for index, subject in enumerate(subjects):
|
|
if not isinstance(subject, Mapping):
|
|
raise DeclarationError(f"manifest.subjects[{index}] must be a mapping")
|
|
subject_id = str(
|
|
_required(subject, "subject_id", f"manifest.subjects[{index}]")
|
|
)
|
|
reference = _validate_workload_ref(
|
|
subject_id, subject.get("workload_ref"), None
|
|
)
|
|
if reference["applicability"] != "not-applicable":
|
|
raise DeclarationError(
|
|
f"manifest.subjects[{index}] is only for explicit not-applicable subjects"
|
|
)
|
|
record = _base_record(
|
|
subject_id,
|
|
str(subject.get("source") or "manifest.subjects"),
|
|
str(subject["source_revision"]) if subject.get("source_revision") else None,
|
|
reference,
|
|
None,
|
|
)
|
|
record.update(
|
|
{
|
|
"workload_id": None,
|
|
"admission": "not-applicable",
|
|
"admission_reason": "catalog_declared_not_applicable",
|
|
"effective_zone": None,
|
|
"membership_revision_reason": "not_applicable",
|
|
"guarantees": [
|
|
"catalog-declared-not-applicable",
|
|
"non-inferred-resolution",
|
|
],
|
|
}
|
|
)
|
|
result["records"].append(record)
|
|
result["records"].sort(key=lambda record: str(record["subject_id"]))
|
|
subject_ids = [str(record["subject_id"]) for record in result["records"]]
|
|
duplicates = sorted(
|
|
subject_id
|
|
for subject_id in set(subject_ids)
|
|
if subject_ids.count(subject_id) > 1
|
|
)
|
|
if duplicates:
|
|
result["errors"].append(
|
|
{
|
|
"source": "manifest",
|
|
"error": f"duplicate subject ids: {duplicates}",
|
|
}
|
|
)
|
|
result["ok"] = False
|
|
result["changes"] = compare_snapshots(result["records"], previous)
|
|
return result
|
|
|
|
|
|
def _load_mapping(path: Path, where: str) -> dict[str, Any]:
|
|
try:
|
|
value = yaml.safe_load(path.read_text()) or {}
|
|
except (OSError, yaml.YAMLError) as exc:
|
|
raise DeclarationError(f"could not read {where}: {exc}") from exc
|
|
if not isinstance(value, dict):
|
|
raise DeclarationError(f"{where} must be a mapping")
|
|
return value
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("paths", nargs="*", type=Path)
|
|
parser.add_argument("--manifest", type=Path)
|
|
parser.add_argument("--source-revision")
|
|
parser.add_argument("--control-profile", type=Path)
|
|
parser.add_argument("--previous", type=Path)
|
|
args = parser.parse_args()
|
|
if bool(args.manifest) == bool(args.paths):
|
|
parser.error("provide either declaration paths or --manifest")
|
|
try:
|
|
profile = (
|
|
_load_mapping(args.control_profile, "control profile")
|
|
if args.control_profile
|
|
else None
|
|
)
|
|
previous = (
|
|
_load_mapping(args.previous, "previous snapshot")
|
|
if args.previous
|
|
else None
|
|
)
|
|
if args.manifest:
|
|
manifest = _load_mapping(args.manifest, "manifest")
|
|
result = resolve_manifest(
|
|
manifest,
|
|
base_dir=args.manifest.parent,
|
|
profile=profile,
|
|
previous=previous,
|
|
)
|
|
else:
|
|
result = resolve_paths(
|
|
args.paths,
|
|
source_revision=args.source_revision,
|
|
profile=profile,
|
|
previous=previous,
|
|
)
|
|
except DeclarationError as exc:
|
|
result = {
|
|
"ok": False,
|
|
"standard": STANDARD,
|
|
"records": [],
|
|
"changes": {"baseline": "initial", "added": [], "removed": [], "changed": []},
|
|
"errors": [{"source": "input", "error": str(exc)}],
|
|
"profile_errors": [],
|
|
}
|
|
print(json.dumps(result, indent=2, sort_keys=True))
|
|
return 0 if result["ok"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|