Files
awoooi/scripts/security/iwooos-owner-gate-guard.py
Your Name 3496a6be65
All checks were successful
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m30s
CD Pipeline / build-and-deploy (push) Successful in 3m49s
CD Pipeline / post-deploy-checks (push) Successful in 1m55s
fix(iwooos): 鎖住 owner gate 與 tenants 前台遮罩
2026-06-15 06:42:25 +08:00

361 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""驗證 IwoooS / S4.9 owner gate 維持只讀收件邊界。
本 guard 只讀取 repo 內的 S4.9 / source-control owner response snapshot
與 Markdown 規範,不送 request、不收 response、不呼叫 Gitea / GitHub /
AwoooP、不修改 repo / refs / workflow / secret / runner也不開 runtime gate。
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
REQUIRED_DOCS = [
"docs/security/S4-9-OWNER-RESPONSE-GATE-CURRENT-GAP-AUDIT.md",
"docs/security/S4-9-CANONICAL-OWNER-RESPONSE-ENVELOPE.md",
"docs/security/S4-9-OWNER-RESPONSE-INTAKE-FORM.md",
"docs/security/S4-9-REVIEWER-VALIDATION-CHECKLIST.md",
"docs/security/S4-9-SECURITY-ACCEPTANCE-RECORD-TEMPLATE.md",
"docs/security/GITEA-INVENTORY-OWNER-ATTESTATION-RESPONSE.md",
"docs/security/SOURCE-CONTROL-OWNER-RESPONSE-VALIDATION-ROLLUP.md",
"docs/security/GITHUB-TARGET-OWNER-DECISION-RESPONSE.md",
"docs/security/SOURCE-CONTROL-REF-TRUTH-OWNER-RESPONSE.md",
"docs/security/SOURCE-CONTROL-WORKFLOW-SECRET-NAME-OWNER-RESPONSE.md",
]
CANONICAL_FIELDS = [
"owner_role_or_team",
"decision",
"decision_reason",
"affected_scope",
"redacted_evidence_refs",
"followup_owner",
]
S4_9_TEMPLATES = [
"response-public-only-vs-local-gitea-gap",
"response-org-user-endpoint-identity",
"response-internal-110-adjacent-scope",
"response-repo-owner-canonical-scope",
"response-legacy-or-inaccessible-disposition",
]
OWNER_PACKET_SPECS = [
{
"label": "s4.9 gitea owner attestation response",
"path": "docs/security/gitea-inventory-owner-attestation-response.snapshot.json",
"schema": "gitea_inventory_owner_attestation_response_v1",
"status": "draft_waiting_owner_response",
"template_count": 5,
"expected_template_ids": S4_9_TEMPLATES,
"summary_counts": {
"owner_response_request_packet_count": 1,
"response_template_count": 5,
"owner_response_template_status_count": 5,
"intake_preflight_check_count": 6,
"intake_outcome_lane_count": 5,
"acceptance_check_count": 8,
"rejection_rule_count": 10,
"received_response_count": 0,
"accepted_response_count": 0,
"rejected_response_count": 0,
"owner_response_metadata_intake_required_count": 6,
"owner_response_metadata_intake_received_count": 0,
"owner_response_metadata_intake_accepted_count": 0,
"owner_response_metadata_intake_runtime_gate_count": 0,
"owner_response_intake_handoff_queue_count": 5,
"owner_response_intake_handoff_queue_received_count": 0,
"owner_response_intake_handoff_queue_accepted_count": 0,
"owner_response_intake_handoff_queue_runtime_gate_count": 0,
},
"false_flags": [
"runtime_execution_authorized",
"action_buttons_allowed",
"token_value_collection_allowed",
"raw_secret_allowed",
"repo_write_allowed",
"refs_sync_allowed",
"github_primary_switch_authorized",
"owner_response_metadata_intake_raw_payload_allowed",
"owner_response_metadata_intake_secret_plaintext_allowed",
"owner_response_metadata_intake_action_buttons_allowed",
"owner_response_intake_handoff_queue_raw_payload_allowed",
"owner_response_intake_handoff_queue_action_buttons_allowed",
],
},
{
"label": "s4.10 github target owner decision response",
"path": "docs/security/github-target-owner-decision-response.snapshot.json",
"schema": "github_target_owner_decision_response_v1",
"status": "draft_waiting_owner_response",
"template_count": 9,
"summary_counts": {
"owner_response_request_packet_count": 1,
"response_template_count": 9,
"owner_response_template_status_count": 9,
"intake_preflight_check_count": 6,
"acceptance_check_count": 8,
"rejection_rule_count": 10,
"received_response_count": 0,
"accepted_response_count": 0,
"rejected_response_count": 0,
},
"false_flags": [
"runtime_execution_authorized",
"action_buttons_allowed",
"repo_creation_authorized",
"visibility_change_authorized",
"refs_sync_authorized",
"github_primary_switch_authorized",
"secret_value_collection_allowed",
"target_owner_request_dispatch_authorized",
],
},
{
"label": "s4.11 ref truth owner response",
"path": "docs/security/source-control-ref-truth-owner-response.snapshot.json",
"schema": "source_control_ref_truth_owner_response_v1",
"status": "draft_waiting_owner_response",
"template_count": 5,
"summary_counts": {
"owner_response_request_packet_count": 1,
"response_template_count": 5,
"owner_response_template_status_count": 5,
"intake_preflight_check_count": 6,
"acceptance_check_count": 8,
"rejection_rule_count": 10,
"received_response_count": 0,
"accepted_response_count": 0,
"rejected_response_count": 0,
},
"false_flags": [
"runtime_execution_authorized",
"action_buttons_allowed",
"refs_sync_authorized",
"refs_delete_authorized",
"force_push_authorized",
"github_primary_switch_authorized",
"secret_value_collection_allowed",
],
},
{
"label": "s4.12 workflow secret owner response",
"path": "docs/security/source-control-workflow-secret-name-owner-response.snapshot.json",
"schema": "source_control_workflow_secret_name_owner_response_v1",
"status": "draft_waiting_owner_response",
"template_count": 5,
"summary_counts": {
"owner_response_request_packet_count": 1,
"response_template_count": 5,
"owner_response_template_status_count": 5,
"intake_preflight_check_count": 6,
"acceptance_check_count": 8,
"rejection_rule_count": 10,
"received_response_count": 0,
"accepted_response_count": 0,
"rejected_response_count": 0,
},
"false_flags": [
"runtime_execution_authorized",
"action_buttons_allowed",
"workflow_modification_authorized",
"repo_secret_change_authorized",
"runner_change_authorized",
"webhook_modification_authorized",
"branch_protection_change_authorized",
"deploy_key_change_authorized",
"github_hosted_runner_enable_authorized",
"secret_value_collection_allowed",
"secret_value_or_hash_collection_allowed",
"workflow_secret_owner_request_dispatch_authorized",
"write_token_allowed",
],
},
]
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_contains(label: str, values: list[Any], expected: Any) -> None:
if expected not in values:
fail(f"{label}: missing {expected!r}")
def assert_text_contains(label: str, text: str, expected: str) -> None:
if expected not in text:
fail(f"{label}: missing {expected!r}")
def assert_path_exists(root: Path, relative_path: str) -> None:
if not (root / relative_path).exists():
fail(f"path missing: {relative_path}")
def summary_value(data: dict[str, Any], key: str) -> Any:
summary = data.get("summary", {})
if key in summary:
return summary[key]
return data.get(key)
def assert_false_summary_flag(label: str, data: dict[str, Any], key: str) -> None:
value = summary_value(data, key)
assert_equal(f"{label}.{key}", value, False)
def validate_docs(root: Path) -> None:
for relative_path in REQUIRED_DOCS:
assert_path_exists(root, relative_path)
canonical_text = (root / "docs/security/S4-9-CANONICAL-OWNER-RESPONSE-ENVELOPE.md").read_text(
encoding="utf-8"
)
intake_text = (root / "docs/security/S4-9-OWNER-RESPONSE-INTAKE-FORM.md").read_text(
encoding="utf-8"
)
gap_text = (root / "docs/security/S4-9-OWNER-RESPONSE-GATE-CURRENT-GAP-AUDIT.md").read_text(
encoding="utf-8"
)
for field in CANONICAL_FIELDS:
assert_text_contains("canonical_envelope.fields", canonical_text, field)
assert_text_contains("intake_form.fields", intake_text, field)
for template_id in S4_9_TEMPLATES:
assert_text_contains("canonical_envelope.templates", canonical_text, template_id)
assert_text_contains("intake_form.templates", intake_text, template_id)
for marker in [
"request_sent=false",
"received_response_count=0",
"accepted_response_count=0",
"runtime_execution_authorized=false",
"action_buttons_allowed=false",
]:
assert_text_contains("canonical_envelope.zero_boundary", canonical_text, marker)
assert_text_contains("intake_form.zero_boundary", intake_text, marker)
assert_text_contains("gap_audit.owner_gate_zero", gap_text, "S4.9 owner response gate 仍是 `0%`")
def validate_gap_audit(root: Path) -> None:
gap = load_json(root / "docs/security/s4-9-owner-response-gap-audit.snapshot.json")
summary = gap["summary"]
assert_equal("gap.schema_version", gap["schema_version"], "s4_9_owner_response_gap_audit_v1")
assert_equal("gap.status", gap["status"], "gap_audit_ready_owner_gate_zero")
assert_equal("gap.summary.current_requirement_gap_count", summary["current_requirement_gap_count"], 8)
assert_equal("gap.summary.new_rule_count", summary["new_rule_count"], 7)
assert_equal("gap.summary.rule_adjustment_count", summary["rule_adjustment_count"], 7)
assert_equal("gap.summary.priority_work_item_count", summary["priority_work_item_count"], 9)
for key in [
"request_sent_count",
"owner_response_received_count",
"owner_response_accepted_count",
"owner_response_rejected_count",
"runtime_gate_count",
]:
assert_equal(f"gap.summary.{key}", summary[key], 0)
assert_equal("gap.summary.public_surface_raw_namespace_allowed", summary["public_surface_raw_namespace_allowed"], False)
assert_equal(
"gap.summary.work_session_transcript_public_allowed",
summary["work_session_transcript_public_allowed"],
False,
)
false_boundaries = gap["false_boundaries"]
for key, value in false_boundaries.items():
if value is not False:
fail(f"gap.false_boundaries.{key}: expected false, got {value!r}")
def validate_owner_packet(root: Path, spec: dict[str, Any]) -> None:
data = load_json(root / spec["path"])
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"])
assert_equal(f"{label}.runtime_execution_authorized", data.get("runtime_execution_authorized"), False)
for key, expected in spec["summary_counts"].items():
assert_equal(f"{label}.summary.{key}", summary_value(data, key), expected)
for key in spec["false_flags"]:
assert_false_summary_flag(label, data, key)
templates = data.get("response_templates", [])
if not isinstance(templates, list):
fail(f"{label}.response_templates: expected list")
assert_equal(f"{label}.response_templates.count", len(templates), spec["template_count"])
if "expected_template_ids" in spec:
template_ids = [item.get("template_id") or item.get("id") for item in templates]
for template_id in spec["expected_template_ids"]:
assert_contains(f"{label}.response_templates", template_ids, template_id)
def validate_rollup(root: Path) -> None:
rollup = load_json(root / "docs/security/source-control-owner-response-validation-rollup.snapshot.json")
summary = rollup["summary"]
assert_equal("rollup.schema_version", rollup["schema_version"], "source_control_owner_response_validation_rollup_v1")
assert_equal("rollup.status", rollup["status"], "draft_waiting_owner_responses")
assert_equal("rollup.summary.response_packet_count", summary["response_packet_count"], 4)
assert_equal("rollup.summary.total_response_template_count", summary["total_response_template_count"], 24)
assert_equal("rollup.summary.total_acceptance_check_count", summary["total_acceptance_check_count"], 32)
assert_equal("rollup.summary.total_rejection_rule_count", summary["total_rejection_rule_count"], 40)
assert_equal("rollup.summary.validation_lane_count", summary["validation_lane_count"], 4)
assert_equal("rollup.summary.owner_response_validation_reviewer_checklist_count", summary["owner_response_validation_reviewer_checklist_count"], 9)
assert_equal("rollup.summary.owner_response_validation_reviewer_outcome_lane_count", summary["owner_response_validation_reviewer_outcome_lane_count"], 7)
for key in [
"total_received_response_count",
"total_accepted_response_count",
"total_rejected_response_count",
"primary_ready_count",
]:
assert_equal(f"rollup.summary.{key}", summary[key], 0)
for key in [
"runtime_execution_authorized",
"action_buttons_allowed",
"repo_creation_authorized",
"visibility_change_authorized",
"refs_sync_authorized",
"refs_delete_authorized",
"workflow_modification_authorized",
"runner_enablement_authorized",
"secret_value_collection_allowed",
"github_primary_switch_authorized",
"force_push_authorized",
"write_token_allowed",
]:
assert_false_summary_flag("rollup", rollup, key)
def validate(root: Path) -> None:
validate_docs(root)
validate_gap_audit(root)
validate_rollup(root)
for spec in OWNER_PACKET_SPECS:
validate_owner_packet(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_OWNER_GATE_GUARD_OK")
if __name__ == "__main__":
main()