Files
awoooi/scripts/security/public-gateway-rendered-diff-acceptance.py
Your Name a4998f915c
All checks were successful
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m32s
CD Pipeline / build-and-deploy (push) Successful in 4m37s
CD Pipeline / post-deploy-checks (push) Successful in 1m31s
fix(iwooos): 新增 public gateway diff evidence acceptance
2026-06-15 00:12:53 +08:00

413 lines
16 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 Public Gateway rendered diff acceptance 只讀帳本產生器。
本工具讀取 Public Gateway rendered diff gate draft 與 owner response
acceptance snapshot定義未來 rendered diff、nginx test evidence 與 route
smoke evidence 如何被收件、補件、隔離或拒收。它不讀 live conf、不產生
diff、不執行 nginx -t、不 reload、不做 route smoke、不連 DNS / TLS。
"""
from __future__ import annotations
import argparse
import json
import subprocess
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
TAIPEI = timezone(timedelta(hours=8))
ACCEPTANCE_FIELDS = [
"diff_acceptance_id",
"owner_response_acceptance_id",
"diff_gate_id",
"config_id",
"control_tier",
"host",
"live_path",
"redacted_live_conf_ref",
"rendered_diff_ref",
"rendered_diff_hash_ref",
"diff_scope_summary",
"affected_routes",
"nginx_test_evidence_ref",
"nginx_test_operator",
"nginx_test_result",
"route_smoke_matrix_ref",
"route_smoke_result_ref",
"tls_acme_impact_ref",
"maintenance_window",
"rollback_owner",
"rollback_ref",
"postcheck_evidence_ref",
"reviewer_outcome",
"followup_owner",
"not_approval",
]
REQUIRED_EVIDENCE_FIELDS = [
"redacted_live_conf_ref",
"rendered_diff_ref",
"rendered_diff_hash_ref",
"diff_scope_summary",
"affected_routes",
"nginx_test_evidence_ref",
"nginx_test_result",
"route_smoke_matrix_ref",
"route_smoke_result_ref",
"tls_acme_impact_ref",
"maintenance_window",
"rollback_owner",
"rollback_ref",
"postcheck_evidence_ref",
]
REVIEWER_CHECKS = [
{
"check_id": "owner_response_accepted_first",
"instruction": "必須先有 owner response accepted record否則不得驗收 rendered diff evidence。",
},
{
"check_id": "redacted_live_conf_ref_only",
"instruction": "只能接受脫敏 live conf ref、hash 或 artifact pointer不得收 raw conf。",
},
{
"check_id": "rendered_diff_ref_not_payload",
"instruction": "rendered diff 必須是 ref / hash不得把完整 diff payload 寫入 repo 或 LOGBOOK。",
},
{
"check_id": "diff_scope_matches_config_id",
"instruction": "diff scope 必須對回 public gateway config_id 與 affected route 清冊。",
},
{
"check_id": "nginx_test_evidence_is_readback_only",
"instruction": "nginx test evidence 只能是 owner 提供的 readback ref不得由本工具執行 nginx -t。",
},
{
"check_id": "nginx_test_result_has_timestamp",
"instruction": "nginx test result 需有時間、環境、操作者角色與結果摘要,但不得含 secret。",
},
{
"check_id": "route_smoke_matrix_complete",
"instruction": "route smoke matrix 必須列 affected routes、預期 status、TLS / WebSocket / ACME checks。",
},
{
"check_id": "tls_acme_impact_separated",
"instruction": "TLS / ACME 影響必須與 reload 決策分離,不能用 route smoke 取代 cert ownership。",
},
{
"check_id": "secret_value_absent",
"instruction": "不得出現 token、cookie、private key、完整憑證內容或 secret derivative。",
},
{
"check_id": "maintenance_window_present",
"instruction": "任何未來 runtime action 前都必須有維護窗口或明確禁止窗口。",
},
{
"check_id": "rollback_owner_and_ref_present",
"instruction": "rollback owner 與 rollback ref 必須存在,且不可指向 raw secret。",
},
{
"check_id": "postcheck_plan_present",
"instruction": "postcheck evidence ref 需描述 API、Web、WebSocket、ACME 或 affected route 的驗證結果。",
},
{
"check_id": "no_execution_request_embedded",
"instruction": "payload 不得夾帶 reload、route change、DNS / TLS probe 或 certbot renew 要求。",
},
{
"check_id": "counts_transition_safe",
"instruction": "只有 reviewer record 可更新 accepted / rejected不得同時開 runtime gate。",
},
{
"check_id": "action_button_absent",
"instruction": "前台與 AwoooP 只能顯示只讀狀態,不得新增執行按鈕。",
},
]
OUTCOME_LANES = [
{
"lane_id": "waiting_owner_response_acceptance",
"meaning": "owner response 尚未 acceptedrendered diff evidence 不可驗收。",
},
{
"lane_id": "waiting_rendered_diff_evidence",
"meaning": "等待 owner-provided rendered diff / nginx test / route smoke evidence ref。",
},
{
"lane_id": "quarantine_raw_conf_or_payload",
"meaning": "收到 raw live conf、完整 diff payload 或不可保存內容時只能隔離。",
},
{
"lane_id": "reject_secret_or_execution_request",
"meaning": "出現 secret value 或夾帶執行要求時直接拒收。",
},
{
"lane_id": "request_evidence_supplement",
"meaning": "欄位不足、scope 不清或 route matrix 不完整時要求補件。",
},
{
"lane_id": "ready_for_reviewer_acceptance",
"meaning": "metadata 合格後只進 reviewer acceptance不自動執行。",
},
{
"lane_id": "accepted_for_runtime_gate_planning",
"meaning": "即使 evidence accepted也只可進下一層 runtime gate planning。",
},
{
"lane_id": "waiting_separate_runtime_approval",
"meaning": "nginx -t、reload、route smoke、DNS / TLS probe 仍需獨立人工批准。",
},
]
BLOCKED_ACTIONS = [
"read_live_conf_over_ssh",
"store_raw_live_conf",
"store_full_rendered_diff_payload",
"accept_unredacted_live_conf",
"collect_secret_value",
"accept_execution_request_inside_evidence",
"mark_rendered_diff_accepted_without_owner_response",
"mark_rendered_diff_accepted_without_reviewer_record",
"run_nginx_test_from_diff_acceptance",
"run_route_smoke_from_diff_acceptance",
"nginx_reload_from_diff_acceptance",
"dns_probe_from_diff_acceptance",
"tls_probe_from_diff_acceptance",
"certbot_renew_from_diff_acceptance",
"modify_nginx_conf",
"modify_dns_tls_config",
"change_public_route",
"change_admin_route",
"change_websocket_route",
"write_production_host",
"open_runtime_gate",
"add_action_button",
]
def git_short_sha(root: Path) -> str:
try:
result = subprocess.run(
["git", "rev-parse", "--short", "HEAD"],
cwd=root,
check=True,
capture_output=True,
text=True,
)
return result.stdout.strip()
except Exception:
return "unknown"
def load_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def candidate_from_owner_acceptance(item: dict[str, Any]) -> dict[str, Any]:
config_id = item["config_id"]
return {
"diff_acceptance_id": f"public_gateway_rendered_diff_acceptance:{config_id}",
"status": "waiting_owner_response_acceptance",
"owner_response_acceptance_id": item["acceptance_candidate_id"],
"diff_gate_id": item["diff_gate_id"],
"config_id": config_id,
"control_tier": item["control_tier"],
"host": item["host"],
"live_path": item["live_path"],
"redacted_live_conf_ref": None,
"rendered_diff_ref": None,
"rendered_diff_hash_ref": None,
"diff_scope_summary": "pending_owner_response_acceptance",
"affected_routes": [],
"nginx_test_evidence_ref": None,
"nginx_test_operator": "pending_runtime_owner",
"nginx_test_result": "pending_owner_provided_readback",
"route_smoke_matrix_ref": None,
"route_smoke_result_ref": None,
"tls_acme_impact_ref": None,
"maintenance_window": "pending_owner_response_acceptance",
"rollback_owner": "pending_owner_response_acceptance",
"rollback_ref": None,
"postcheck_evidence_ref": None,
"reviewer_outcome": "waiting_owner_response_acceptance",
"followup_owner": "pending_owner_response_acceptance",
"acceptance_fields": ACCEPTANCE_FIELDS,
"required_evidence_fields": REQUIRED_EVIDENCE_FIELDS,
"reviewer_checks": [item["check_id"] for item in REVIEWER_CHECKS],
"outcome_lanes": [item["lane_id"] for item in OUTCOME_LANES],
"blocked_actions": BLOCKED_ACTIONS,
"not_approval": True,
"owner_response_accepted": False,
"redacted_export_accepted": False,
"rendered_diff_received": False,
"rendered_diff_accepted": False,
"nginx_test_evidence_received": False,
"nginx_test_evidence_accepted": False,
"route_smoke_matrix_received": False,
"route_smoke_matrix_accepted": False,
"route_smoke_result_received": False,
"route_smoke_result_accepted": False,
"tls_acme_impact_accepted": False,
"maintenance_window_accepted": False,
"rollback_owner_accepted": False,
"postcheck_evidence_accepted": False,
"nginx_test_authorized": False,
"nginx_test_executed": False,
"nginx_reload_authorized": False,
"nginx_reload_executed": False,
"route_smoke_authorized": False,
"route_smoke_executed": False,
"dns_tls_probe_authorized": False,
"certbot_renew_authorized": False,
"production_write_authorized": False,
"runtime_gate": False,
"action_buttons_allowed": False,
}
def build_report(
root: Path,
diff_gate_report: dict[str, Any],
owner_acceptance_report: dict[str, Any],
generated_at: str | None,
) -> dict[str, Any]:
report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
owner_candidates = owner_acceptance_report.get("acceptance_candidates", [])
acceptance_candidates = [candidate_from_owner_acceptance(item) for item in owner_candidates]
c0_candidates = [item for item in acceptance_candidates if item["control_tier"] == "C0"]
c1_candidates = [item for item in acceptance_candidates if item["control_tier"] == "C1"]
return {
"schema_version": "public_gateway_rendered_diff_acceptance_v1",
"generated_at": report_time,
"git_commit": git_short_sha(root),
"source_rendered_diff_gate_schema_version": diff_gate_report.get("schema_version"),
"source_rendered_diff_gate_status": diff_gate_report.get("status"),
"source_owner_response_acceptance_schema_version": owner_acceptance_report.get("schema_version"),
"source_owner_response_acceptance_status": owner_acceptance_report.get("status"),
"status": "rendered_diff_acceptance_ledger_ready_no_runtime_action",
"summary": {
"source_diff_gate_candidate_count": diff_gate_report.get("summary", {}).get(
"diff_gate_candidate_count", 0
),
"source_owner_response_acceptance_candidate_count": owner_acceptance_report.get(
"summary", {}
).get("acceptance_candidate_count", 0),
"diff_acceptance_candidate_count": len(acceptance_candidates),
"c0_diff_acceptance_candidate_count": len(c0_candidates),
"c1_diff_acceptance_candidate_count": len(c1_candidates),
"diff_acceptance_field_count": len(ACCEPTANCE_FIELDS),
"required_evidence_field_count": len(REQUIRED_EVIDENCE_FIELDS),
"reviewer_check_count": len(REVIEWER_CHECKS),
"outcome_lane_count": len(OUTCOME_LANES),
"blocked_action_count": len(BLOCKED_ACTIONS),
"owner_response_accepted_count": 0,
"redacted_export_accepted_count": 0,
"rendered_diff_received_count": 0,
"rendered_diff_accepted_count": 0,
"nginx_test_evidence_received_count": 0,
"nginx_test_evidence_accepted_count": 0,
"route_smoke_matrix_received_count": 0,
"route_smoke_matrix_accepted_count": 0,
"route_smoke_result_received_count": 0,
"route_smoke_result_accepted_count": 0,
"tls_acme_impact_accepted_count": 0,
"maintenance_window_accepted_count": 0,
"rollback_owner_accepted_count": 0,
"postcheck_evidence_accepted_count": 0,
"nginx_test_authorized_count": 0,
"nginx_test_executed_count": 0,
"nginx_reload_authorized_count": 0,
"nginx_reload_executed_count": 0,
"route_smoke_authorized_count": 0,
"route_smoke_executed_count": 0,
"dns_tls_probe_authorized_count": 0,
"certbot_renew_authorized_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
},
"execution_boundaries": {
"read_live_conf_over_ssh": False,
"store_raw_live_conf": False,
"store_full_rendered_diff_payload": False,
"secret_value_collection_allowed": False,
"rendered_diff_accepted": False,
"nginx_test_authorized": False,
"nginx_test_executed": False,
"nginx_reload_authorized": False,
"nginx_reload_executed": False,
"route_smoke_authorized": False,
"route_smoke_executed": False,
"dns_tls_probe_authorized": False,
"certbot_renew_authorized": False,
"production_write_authorized": False,
"runtime_execution_authorized": False,
"action_buttons_allowed": False,
"not_authorization": True,
},
"diff_acceptance_fields": ACCEPTANCE_FIELDS,
"required_evidence_fields": REQUIRED_EVIDENCE_FIELDS,
"reviewer_checks": REVIEWER_CHECKS,
"outcome_lanes": OUTCOME_LANES,
"blocked_actions": BLOCKED_ACTIONS,
"diff_acceptance_candidates": acceptance_candidates,
"next_steps": [
"等待 owner response accepted未 accepted 前不得驗收 rendered diff evidence。",
"收到 rendered diff / nginx test / route smoke evidence 後,先做 raw payload、secret、scope 與 route matrix 檢查。",
"evidence accepted 也只能進 runtime gate planning`nginx -t`、reload、route smoke、DNS / TLS probe 與 production write 仍需獨立人工批准。",
],
}
def main() -> int:
parser = argparse.ArgumentParser(description="IwoooS Public Gateway rendered diff acceptance 只讀帳本產生器")
parser.add_argument("--root", default=".", help="repo root")
parser.add_argument(
"--rendered-diff-gate-report",
default="docs/security/public-gateway-rendered-diff-gate-draft.snapshot.json",
help="public-gateway-rendered-diff-gate-draft.py 輸出的 JSON",
)
parser.add_argument(
"--owner-response-acceptance-report",
default="docs/security/public-gateway-owner-response-acceptance.snapshot.json",
help="public-gateway-owner-response-acceptance.py 輸出的 JSON",
)
parser.add_argument("--output", help="寫出 JSON 報告")
parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用")
args = parser.parse_args()
root = Path(args.root).resolve()
diff_gate_report = load_json(root / args.rendered_diff_gate_report)
owner_acceptance_report = load_json(root / args.owner_response_acceptance_report)
report = build_report(root, diff_gate_report, owner_acceptance_report, args.generated_at)
payload = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)
if args.output:
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(payload + "\n", encoding="utf-8")
else:
print(payload)
summary = report["summary"]
print(
"PUBLIC_GATEWAY_RENDERED_DIFF_ACCEPTANCE_OK "
f"candidates={summary['diff_acceptance_candidate_count']} "
f"c0={summary['c0_diff_acceptance_candidate_count']} "
f"checks={summary['reviewer_check_count']} "
f"lanes={summary['outcome_lane_count']} "
f"accepted={summary['rendered_diff_accepted_count']} "
f"runtime_gate={summary['runtime_gate_count']}",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())