536 lines
22 KiB
Python
536 lines
22 KiB
Python
#!/usr/bin/env python3
|
||
"""驗證 IwoooS 高價值配置控管 snapshot 維持只讀邊界。
|
||
|
||
本 guard 只讀取 repo 內已提交的 Markdown / JSON snapshot,不連線主機、不
|
||
SSH、不讀 secret、不執行 Nginx / K8s / workflow / runner / backup / scan
|
||
動作。它的用途是把 Nginx、TLS、K8s、Secrets、runner、防火牆、backup、
|
||
monitoring、public runtime 與 agent-bounty-protocol 等高價值配置,固定成
|
||
可重複驗證的資安控管基線。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
|
||
EXPECTED_CATEGORIES = {
|
||
"nginx_public_gateway": 90,
|
||
"dns_tls_certbot": 78,
|
||
"k8s_production_gitops": 64,
|
||
"secret_metadata": 68,
|
||
"gitea_workflow_runner_source_control": 72,
|
||
"public_admin_api_runtime_config": 64,
|
||
"backup_restore_credential": 62,
|
||
"agent_bounty_protocol_runtime": 68,
|
||
"monitoring_alerting_observability": 66,
|
||
"docker_compose_systemd_host_config": 54,
|
||
"ssh_firewall_network_access": 62,
|
||
"ai_provider_model_routing": 60,
|
||
"product_surface_runtime_routes": 72,
|
||
"security_evidence_tooling": 86,
|
||
}
|
||
|
||
REQUIRED_C0_CATEGORIES = {
|
||
"nginx_public_gateway",
|
||
"dns_tls_certbot",
|
||
"k8s_production_gitops",
|
||
"secret_metadata",
|
||
"gitea_workflow_runner_source_control",
|
||
"public_admin_api_runtime_config",
|
||
"backup_restore_credential",
|
||
"agent_bounty_protocol_runtime",
|
||
}
|
||
|
||
REQUIRED_CONTROL_DOCS = [
|
||
"docs/security/HIGH-VALUE-CONFIG-CONTROL-COVERAGE.md",
|
||
"docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md",
|
||
"docs/security/PUBLIC-GATEWAY-PREFLIGHT-INVENTORY.md",
|
||
"docs/security/PUBLIC-GATEWAY-OWNER-RESPONSE-ACCEPTANCE.md",
|
||
"docs/security/PUBLIC-GATEWAY-RENDERED-DIFF-ACCEPTANCE.md",
|
||
"docs/security/DOMAIN-TLS-CERTBOT-OWNER-RESPONSE-ACCEPTANCE.md",
|
||
"docs/security/HOST-SERVICE-OWNER-RESPONSE-ACCEPTANCE.md",
|
||
"docs/security/SSH-NETWORK-OWNER-RESPONSE-ACCEPTANCE.md",
|
||
"docs/security/PORT-FIREWALL-CHANGE-EVIDENCE-ACCEPTANCE.md",
|
||
"docs/security/BACKUP-RESTORE-OWNER-RESPONSE-ACCEPTANCE.md",
|
||
"docs/security/K8S-ARGOCD-OWNER-RESPONSE-ACCEPTANCE.md",
|
||
"docs/security/K8S-ARGOCD-CHANGE-EVIDENCE-ACCEPTANCE.md",
|
||
"docs/security/CD-RUNNER-SECRET-INJECTION-CHANGE-EVIDENCE-ACCEPTANCE.md",
|
||
"docs/security/PUBLIC-RUNTIME-CONFIG-CHANGE-EVIDENCE-ACCEPTANCE.md",
|
||
"docs/security/MONITORING-OWNER-REQUEST-DRAFT.md",
|
||
"docs/security/AGENT-BOUNTY-OWNER-REQUEST-DRAFT.md",
|
||
"docs/security/SECURITY-SUPPLY-CHAIN-CONTRACT-MANIFEST.md",
|
||
]
|
||
|
||
SUMMARY_ZERO_MARKERS = (
|
||
"_authorized_count",
|
||
"_executed_count",
|
||
"_received_count",
|
||
"_accepted_count",
|
||
"_allowed_count",
|
||
"_confirmed_count",
|
||
"_quarantined_count",
|
||
"_rejected_count",
|
||
"_ready_count",
|
||
)
|
||
|
||
SUMMARY_ZERO_KEYS = {
|
||
"action_button_count",
|
||
"request_sent_count",
|
||
"runtime_gate_count",
|
||
"runtime_approval_package_ready_count",
|
||
"supplement_requested_count",
|
||
"impact_supplement_requested_count",
|
||
}
|
||
|
||
TRUE_BOUNDARY_KEYS = {"not_authorization"}
|
||
|
||
FALSE_BOUNDARY_KEYS = [
|
||
"runtime_execution_authorized",
|
||
"host_write_authorized",
|
||
"nginx_reload_authorized",
|
||
"public_gateway_reload_authorized",
|
||
"certbot_renew_authorized",
|
||
"argocd_sync_authorized",
|
||
"kubectl_action_authorized",
|
||
"workflow_modification_authorized",
|
||
"runner_change_authorized",
|
||
"secret_value_collection_allowed",
|
||
"backup_run_authorized",
|
||
"restore_run_authorized",
|
||
"active_scan_authorized",
|
||
"action_buttons_allowed",
|
||
]
|
||
|
||
ARTIFACT_SPECS = [
|
||
{
|
||
"label": "public gateway owner response acceptance",
|
||
"path": "docs/security/public-gateway-owner-response-acceptance.snapshot.json",
|
||
"schema": "public_gateway_owner_response_acceptance_v1",
|
||
"status": "owner_response_acceptance_ledger_ready_no_runtime_action",
|
||
"list_counts": {
|
||
"acceptance_candidates": 3,
|
||
"blocked_actions": 28,
|
||
"reviewer_checks": 22,
|
||
"outcome_lanes": 8,
|
||
"required_owner_response_fields": 22,
|
||
},
|
||
"summary_counts": {
|
||
"acceptance_candidate_count": 3,
|
||
"c0_acceptance_candidate_count": 2,
|
||
"owner_response_received_count": 0,
|
||
"owner_response_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
{
|
||
"label": "public gateway rendered diff acceptance",
|
||
"path": "docs/security/public-gateway-rendered-diff-acceptance.snapshot.json",
|
||
"schema": "public_gateway_rendered_diff_acceptance_v1",
|
||
"status": "rendered_diff_acceptance_ledger_ready_no_runtime_action",
|
||
"list_counts": {
|
||
"blocked_actions": 22,
|
||
"reviewer_checks": 15,
|
||
"outcome_lanes": 8,
|
||
"required_evidence_fields": 14,
|
||
},
|
||
"summary_counts": {
|
||
"diff_acceptance_candidate_count": 3,
|
||
"c0_diff_acceptance_candidate_count": 2,
|
||
"rendered_diff_received_count": 0,
|
||
"rendered_diff_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
{
|
||
"label": "domain tls certbot owner response acceptance",
|
||
"path": "docs/security/domain-tls-certbot-owner-response-acceptance.snapshot.json",
|
||
"schema": "domain_tls_certbot_owner_response_acceptance_v1",
|
||
"status": "owner_response_acceptance_ledger_ready_no_runtime_action",
|
||
"list_counts": {
|
||
"acceptance_candidates": 4,
|
||
"blocked_actions": 20,
|
||
"reviewer_checks": 13,
|
||
"outcome_lanes": 7,
|
||
"required_owner_response_fields": 13,
|
||
},
|
||
"summary_counts": {
|
||
"acceptance_candidate_count": 4,
|
||
"c0_acceptance_candidate_count": 4,
|
||
"owner_response_received_count": 0,
|
||
"owner_response_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
{
|
||
"label": "host service owner response acceptance",
|
||
"path": "docs/security/host-service-owner-response-acceptance.snapshot.json",
|
||
"schema": "host_service_owner_response_acceptance_v1",
|
||
"status": "owner_response_acceptance_ledger_ready_no_runtime_action",
|
||
"list_counts": {
|
||
"acceptance_candidates": 9,
|
||
"blocked_actions": 20,
|
||
"reviewer_checks": 14,
|
||
"outcome_lanes": 7,
|
||
},
|
||
"summary_counts": {
|
||
"acceptance_candidate_count": 9,
|
||
"write_capable_acceptance_candidate_count": 3,
|
||
"owner_response_received_count": 0,
|
||
"owner_response_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
{
|
||
"label": "ssh network owner response acceptance",
|
||
"path": "docs/security/ssh-network-owner-response-acceptance.snapshot.json",
|
||
"schema": "ssh_network_owner_response_acceptance_v1",
|
||
"status": "owner_response_acceptance_ledger_ready_no_runtime_action",
|
||
"list_counts": {
|
||
"acceptance_candidates": 16,
|
||
"blocked_actions": 22,
|
||
"reviewer_checks": 15,
|
||
"outcome_lanes": 7,
|
||
"required_owner_fields": 13,
|
||
},
|
||
"summary_counts": {
|
||
"acceptance_candidate_count": 16,
|
||
"write_capable_acceptance_candidate_count": 6,
|
||
"owner_response_received_count": 0,
|
||
"owner_response_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
{
|
||
"label": "port firewall change evidence acceptance",
|
||
"path": "docs/security/port-firewall-change-evidence-acceptance.snapshot.json",
|
||
"schema": "port_firewall_change_evidence_acceptance_v1",
|
||
"status": "change_evidence_acceptance_ready_no_runtime_action",
|
||
"list_counts": {
|
||
"change_evidence_candidates": 14,
|
||
"blocked_actions": 28,
|
||
"reviewer_checks": 21,
|
||
"outcome_lanes": 9,
|
||
"required_evidence_fields": 21,
|
||
},
|
||
"summary_counts": {
|
||
"change_evidence_candidate_count": 14,
|
||
"write_capable_change_evidence_candidate_count": 6,
|
||
"change_evidence_received_count": 0,
|
||
"change_evidence_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
{
|
||
"label": "backup restore owner response acceptance",
|
||
"path": "docs/security/backup-restore-owner-response-acceptance.snapshot.json",
|
||
"schema": "backup_restore_owner_response_acceptance_v1",
|
||
"status": "owner_response_acceptance_ledger_ready_no_runtime_action",
|
||
"list_counts": {
|
||
"acceptance_candidates": 38,
|
||
"blocked_actions": 22,
|
||
"reviewer_checks": 13,
|
||
"outcome_lanes": 7,
|
||
"required_owner_fields": 14,
|
||
},
|
||
"summary_counts": {
|
||
"acceptance_candidate_count": 38,
|
||
"write_capable_acceptance_candidate_count": 27,
|
||
"owner_response_received_count": 0,
|
||
"owner_response_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
{
|
||
"label": "k8s argocd owner response acceptance",
|
||
"path": "docs/security/k8s-argocd-owner-response-acceptance.snapshot.json",
|
||
"schema": "k8s_argocd_owner_response_acceptance_v1",
|
||
"status": "owner_response_acceptance_ledger_ready_no_runtime_action",
|
||
"list_counts": {
|
||
"acceptance_candidates": 4,
|
||
"blocked_actions": 18,
|
||
"reviewer_checks": 12,
|
||
"outcome_lanes": 7,
|
||
"required_owner_fields": 11,
|
||
},
|
||
"summary_counts": {
|
||
"acceptance_candidate_count": 4,
|
||
"c0_acceptance_candidate_count": 3,
|
||
"owner_response_received_count": 0,
|
||
"owner_response_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
{
|
||
"label": "k8s argocd change evidence acceptance",
|
||
"path": "docs/security/k8s-argocd-change-evidence-acceptance.snapshot.json",
|
||
"schema": "k8s_argocd_change_evidence_acceptance_v1",
|
||
"status": "change_evidence_acceptance_ledger_ready_no_runtime_action",
|
||
"list_counts": {
|
||
"change_evidence_candidates": 4,
|
||
"blocked_actions": 28,
|
||
"reviewer_checks": 18,
|
||
"outcome_lanes": 8,
|
||
"required_evidence_fields": 18,
|
||
},
|
||
"summary_counts": {
|
||
"change_evidence_candidate_count": 4,
|
||
"c0_change_evidence_candidate_count": 3,
|
||
"change_evidence_received_count": 0,
|
||
"change_evidence_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
{
|
||
"label": "cd runner secret injection change evidence acceptance",
|
||
"path": "docs/security/cd-runner-secret-injection-change-evidence-acceptance.snapshot.json",
|
||
"schema": "cd_runner_secret_injection_change_evidence_acceptance_v1",
|
||
"status": "change_evidence_acceptance_ledger_ready_no_runtime_action",
|
||
"list_counts": {
|
||
"change_evidence_candidates": 5,
|
||
"blocked_actions": 32,
|
||
"reviewer_checks": 19,
|
||
"outcome_lanes": 8,
|
||
"required_evidence_fields": 19,
|
||
},
|
||
"summary_counts": {
|
||
"change_evidence_candidate_count": 5,
|
||
"c0_change_evidence_candidate_count": 4,
|
||
"change_evidence_received_count": 0,
|
||
"change_evidence_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
{
|
||
"label": "public runtime config change evidence acceptance",
|
||
"path": "docs/security/public-runtime-config-change-evidence-acceptance.snapshot.json",
|
||
"schema": "public_runtime_config_change_evidence_acceptance_v1",
|
||
"status": "change_evidence_acceptance_ledger_ready_no_runtime_action",
|
||
"list_counts": {
|
||
"change_evidence_candidates": 6,
|
||
"blocked_actions": 32,
|
||
"reviewer_checks": 21,
|
||
"outcome_lanes": 8,
|
||
"required_evidence_fields": 21,
|
||
},
|
||
"summary_counts": {
|
||
"change_evidence_candidate_count": 6,
|
||
"c0_change_evidence_candidate_count": 5,
|
||
"change_evidence_received_count": 0,
|
||
"change_evidence_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
{
|
||
"label": "monitoring owner request draft",
|
||
"path": "docs/security/monitoring-owner-request-draft.snapshot.json",
|
||
"schema": "monitoring_owner_request_draft_v1",
|
||
"status": "owner_request_draft_ready_not_dispatched",
|
||
"list_counts": {
|
||
"request_drafts": 60,
|
||
"blocked_actions": 24,
|
||
"required_owner_fields": 14,
|
||
},
|
||
"summary_counts": {
|
||
"request_draft_count": 60,
|
||
"write_capable_request_draft_count": 11,
|
||
"owner_response_received_count": 0,
|
||
"owner_response_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
{
|
||
"label": "monitoring owner response acceptance",
|
||
"path": "docs/security/monitoring-owner-response-acceptance.snapshot.json",
|
||
"schema": "monitoring_owner_response_acceptance_v1",
|
||
"status": "owner_response_acceptance_ledger_ready_no_runtime_action",
|
||
"list_counts": {
|
||
"acceptance_candidates": 60,
|
||
"blocked_actions": 28,
|
||
"reviewer_checks": 15,
|
||
"outcome_lanes": 7,
|
||
"required_owner_fields": 14,
|
||
},
|
||
"summary_counts": {
|
||
"acceptance_candidate_count": 60,
|
||
"write_capable_acceptance_candidate_count": 11,
|
||
"owner_response_received_count": 0,
|
||
"owner_response_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
{
|
||
"label": "agent bounty owner request draft",
|
||
"path": "docs/security/agent-bounty-owner-request-draft.snapshot.json",
|
||
"schema": "agent_bounty_owner_request_draft_v1",
|
||
"status": "owner_request_draft_ready_not_dispatched",
|
||
"list_counts": {
|
||
"request_drafts": 11,
|
||
"blocked_actions": 28,
|
||
"required_owner_fields": 22,
|
||
},
|
||
"summary_counts": {
|
||
"request_draft_count": 11,
|
||
"write_capable_request_draft_count": 8,
|
||
"owner_response_received_count": 0,
|
||
"owner_response_accepted_count": 0,
|
||
"runtime_gate_count": 0,
|
||
},
|
||
},
|
||
]
|
||
|
||
|
||
def load_json(path: Path) -> dict[str, Any]:
|
||
return json.loads(path.read_text(encoding="utf-8"))
|
||
|
||
|
||
def fail(message: str) -> None:
|
||
raise SystemExit(f"BLOCKED {message}")
|
||
|
||
|
||
def assert_equal(label: str, actual: Any, expected: Any) -> None:
|
||
if actual != expected:
|
||
fail(f"{label}: expected {expected!r}, got {actual!r}")
|
||
|
||
|
||
def assert_at_least(label: str, actual: int, expected_minimum: int) -> None:
|
||
if actual < expected_minimum:
|
||
fail(f"{label}: expected >= {expected_minimum!r}, got {actual!r}")
|
||
|
||
|
||
def assert_path_exists(root: Path, relative_path: str) -> None:
|
||
path = root / relative_path
|
||
if not path.exists():
|
||
fail(f"path missing: {relative_path}")
|
||
|
||
|
||
def assert_summary_zero_boundaries(label: str, summary: dict[str, Any]) -> None:
|
||
for key, value in summary.items():
|
||
if not isinstance(value, int):
|
||
continue
|
||
should_be_zero = key in SUMMARY_ZERO_KEYS or any(key.endswith(marker) for marker in SUMMARY_ZERO_MARKERS)
|
||
if should_be_zero and value != 0:
|
||
fail(f"{label}.summary.{key}: expected 0, got {value!r}")
|
||
|
||
|
||
def assert_execution_boundaries_false(label: str, data: dict[str, Any]) -> None:
|
||
boundaries = data.get("execution_boundaries", {})
|
||
if not isinstance(boundaries, dict):
|
||
return
|
||
for key, value in boundaries.items():
|
||
if key in TRUE_BOUNDARY_KEYS:
|
||
if value is not True:
|
||
fail(f"{label}.execution_boundaries.{key}: expected true, got {value!r}")
|
||
continue
|
||
if value is not False:
|
||
fail(f"{label}.execution_boundaries.{key}: expected false, got {value!r}")
|
||
|
||
|
||
def validate_coverage_snapshot(root: Path) -> None:
|
||
coverage_path = root / "docs/security/high-value-config-control-coverage.snapshot.json"
|
||
coverage = load_json(coverage_path)
|
||
summary = coverage["summary"]
|
||
|
||
assert_equal("coverage.schema_version", coverage["schema_version"], "high_value_config_control_coverage_v1")
|
||
assert_equal("coverage.status", coverage["status"], "coverage_matrix_ready")
|
||
assert_equal("coverage.source_category_definition", coverage["source_category_definition"], "scripts/security/high-value-config-change-gate.py")
|
||
assert_equal("coverage.summary.category_count", summary["category_count"], len(EXPECTED_CATEGORIES))
|
||
assert_equal("coverage.summary.registered_control_count", summary["registered_control_count"], len(EXPECTED_CATEGORIES))
|
||
assert_equal("coverage.summary.c0_category_count", summary["c0_category_count"], len(REQUIRED_C0_CATEGORIES))
|
||
assert_equal("coverage.summary.c1_category_count", summary["c1_category_count"], 4)
|
||
assert_equal("coverage.summary.c2_category_count", summary["c2_category_count"], 1)
|
||
assert_equal("coverage.summary.c3_category_count", summary["c3_category_count"], 1)
|
||
assert_equal("coverage.summary.owner_response_required_count", summary["owner_response_required_count"], len(EXPECTED_CATEGORIES))
|
||
assert_equal("coverage.summary.owner_response_received_count", summary["owner_response_received_count"], 0)
|
||
assert_equal("coverage.summary.owner_response_accepted_count", summary["owner_response_accepted_count"], 0)
|
||
assert_equal("coverage.summary.runtime_gate_count", summary["runtime_gate_count"], 0)
|
||
assert_equal("coverage.summary.action_button_count", summary["action_button_count"], 0)
|
||
assert_at_least("coverage.summary.average_coverage_percent", summary["average_coverage_percent"], 68)
|
||
assert_at_least("coverage.summary.needs_live_evidence_count", summary["needs_live_evidence_count"], 9)
|
||
|
||
categories = {item["category_id"]: item for item in coverage["coverage_categories"]}
|
||
assert_equal("coverage.category ids", set(categories), set(EXPECTED_CATEGORIES))
|
||
for category_id, expected_minimum in EXPECTED_CATEGORIES.items():
|
||
category = categories[category_id]
|
||
assert_at_least(f"coverage.{category_id}.coverage_percent", category["coverage_percent"], expected_minimum)
|
||
assert_equal(f"coverage.{category_id}.owner_response_required", category["owner_response_required"], True)
|
||
if category_id in REQUIRED_C0_CATEGORIES:
|
||
assert_equal(f"coverage.{category_id}.control_tier", category["control_tier"], "C0")
|
||
for ref in category.get("evidence_refs", []):
|
||
if ref.startswith(("docs/", "scripts/", "k8s/", "infra/", "ops/")):
|
||
assert_path_exists(root, ref)
|
||
|
||
assert_execution_boundaries_false("coverage", coverage)
|
||
boundaries = coverage["execution_boundaries"]
|
||
for key in FALSE_BOUNDARY_KEYS:
|
||
assert_equal(f"coverage.execution_boundaries.{key}", boundaries.get(key), False)
|
||
|
||
|
||
def validate_artifact_spec(root: Path, spec: dict[str, Any]) -> None:
|
||
path = root / spec["path"]
|
||
assert_path_exists(root, spec["path"])
|
||
data = load_json(path)
|
||
summary = data.get("summary", {})
|
||
label = spec["label"]
|
||
|
||
assert_equal(f"{label}.schema_version", data.get("schema_version"), spec["schema"])
|
||
assert_equal(f"{label}.status", data.get("status"), spec["status"])
|
||
for key, expected_count in spec.get("list_counts", {}).items():
|
||
value = data.get(key)
|
||
if not isinstance(value, list):
|
||
fail(f"{label}.{key}: expected list, got {type(value).__name__}")
|
||
assert_equal(f"{label}.{key}.count", len(value), expected_count)
|
||
for key, expected_value in spec.get("summary_counts", {}).items():
|
||
assert_equal(f"{label}.summary.{key}", summary.get(key), expected_value)
|
||
assert_summary_zero_boundaries(label, summary)
|
||
assert_execution_boundaries_false(label, data)
|
||
|
||
|
||
def validate_supply_chain_manifest(root: Path) -> None:
|
||
manifest = load_json(root / "docs/security/security-supply-chain-contract-manifest.snapshot.json")
|
||
assert_equal("supply_chain.schema_version", manifest["schema_version"], "security_supply_chain_contract_manifest_v1")
|
||
assert_equal("supply_chain.default_enforcement_level", manifest["default_enforcement_level"], "mirror_only")
|
||
assert_equal("supply_chain.contract_count", manifest["contract_count"], 36)
|
||
assert_equal("supply_chain.contract list count", len(manifest["contracts"]), manifest["contract_count"])
|
||
|
||
for item in manifest["contracts"]:
|
||
contract = item["contract"]
|
||
if not item.get("forbidden_actions"):
|
||
fail(f"supply_chain.{contract}.forbidden_actions: expected non-empty list")
|
||
for ref_key in ["snapshot_paths", "human_docs"]:
|
||
for ref in item.get(ref_key, []):
|
||
assert_path_exists(root, ref)
|
||
schema_path = item.get("schema_path")
|
||
if schema_path:
|
||
assert_path_exists(root, schema_path)
|
||
if item.get("consumption_mode") not in {"mirror_only", "read_only_policy", "approval_only", "suggest_only"}:
|
||
fail(f"supply_chain.{contract}.consumption_mode: unsafe value {item.get('consumption_mode')!r}")
|
||
|
||
|
||
def validate(root: Path) -> None:
|
||
for relative_path in REQUIRED_CONTROL_DOCS:
|
||
assert_path_exists(root, relative_path)
|
||
validate_coverage_snapshot(root)
|
||
validate_supply_chain_manifest(root)
|
||
for spec in ARTIFACT_SPECS:
|
||
validate_artifact_spec(root, spec)
|
||
|
||
|
||
def main() -> None:
|
||
parser = argparse.ArgumentParser(description=__doc__)
|
||
parser.add_argument(
|
||
"--root",
|
||
default=Path(__file__).resolve().parents[2],
|
||
type=Path,
|
||
help="Repository root. Defaults to the current script's repository.",
|
||
)
|
||
args = parser.parse_args()
|
||
validate(args.root.resolve())
|
||
print("IWOOOS_CONFIG_CONTROL_GUARD_OK")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|