Files
awoooi/scripts/security/public-gateway-rendered-diff-gate-draft.py

263 lines
9.6 KiB
Python
Raw Permalink 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 gate 草稿產生器。
本工具讀取 redacted export intake preflight snapshot產生未來 rendered
diff、nginx -t、reload 與 route smoke 的分階段 gate 草稿。它不讀 live
conf、不產生 diff、不執行 nginx -t、不 reload、不做 route smoke。
"""
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))
DIFF_GATE_FIELDS = [
"diff_gate_id",
"intake_id",
"export_request_id",
"config_id",
"control_tier",
"source_config_ref",
"redacted_live_conf_ref",
"rendered_diff_ref",
"nginx_test_plan_ref",
"route_smoke_plan_ref",
"rollback_owner",
"not_approval",
]
PREFLIGHT_STAGES = [
(
"redacted_export_acceptance_required",
"必須先有合格 redacted export accepted metadata否則不得產生 rendered diff。",
),
(
"normalize_without_raw_conf_storage",
"只可在隔離工作區以脫敏 ref 產生 normalized diff不得把 raw live conf 寫入 repo。",
),
(
"rendered_diff_owner_review_required",
"rendered diff 只可成為 owner review candidate不自動批准。",
),
(
"nginx_test_approval_package_required",
"`nginx -t` 必須另有人工批准包、rollback owner 與維護窗口。",
),
(
"reload_approval_separate",
"reload 與 public route change 必須獨立於 rendered diff 與 nginx -t。",
),
(
"route_smoke_matrix_required",
"route smoke 需列出 affected routes、預期 status、TLS / WebSocket / ACME checks。",
),
(
"postcheck_and_rollback_required",
"任何未來執行前都需 rollback owner、post-check 與失敗撤回條件。",
),
]
BLOCKED_ACTIONS = [
"read_live_conf_over_ssh",
"store_raw_live_conf",
"render_diff_from_unredacted_payload",
"nginx_test_without_approval",
"nginx_reload_without_approval",
"route_smoke_without_plan",
"dns_probe_without_approval",
"tls_probe_without_approval",
"certbot_renew_without_approval",
"modify_nginx_conf",
"modify_dns_tls_config",
"change_public_route",
"write_production_host",
"open_runtime_gate",
]
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 preflight_stages() -> list[dict[str, Any]]:
return [
{
"stage_id": stage_id,
"status": "required_before_runtime_action",
"instruction": instruction,
"gate_effect": "不增加 rendered_diff / nginx_test / reload / route_smoke / runtime gate。",
}
for stage_id, instruction in PREFLIGHT_STAGES
]
def diff_gate_candidate(intake: dict[str, Any]) -> dict[str, Any]:
config_id = intake["config_id"]
return {
"diff_gate_id": f"public_gateway_rendered_diff_gate:{config_id}",
"status": "draft_waiting_redacted_export_acceptance",
"intake_id": intake["intake_id"],
"export_request_id": intake["export_request_id"],
"config_id": config_id,
"host": intake["host"],
"live_path": intake["live_path"],
"control_tier": intake["control_tier"],
"owner_gate": intake["owner_gate"],
"source_config_ref": "docs/security/public-gateway-preflight-inventory.snapshot.json",
"redacted_live_conf_ref": None,
"rendered_diff_ref": None,
"nginx_test_plan_ref": None,
"route_smoke_plan_ref": None,
"rollback_owner": "pending_rollback_owner",
"diff_gate_fields": DIFF_GATE_FIELDS,
"preflight_stages": [stage_id for stage_id, _instruction in PREFLIGHT_STAGES],
"blocked_actions": BLOCKED_ACTIONS,
"not_approval": True,
"redacted_export_accepted": False,
"rendered_diff_candidate": False,
"rendered_diff_ready": 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,
"maintenance_window_accepted": False,
"rollback_owner_accepted": False,
"runtime_gate": False,
"action_buttons_allowed": False,
"production_write_authorized": False,
}
def build_report(root: Path, intake_report: dict[str, Any], generated_at: str | None) -> dict[str, Any]:
report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
intake_candidates = intake_report.get("intake_candidates", [])
diff_gate_candidates = [diff_gate_candidate(item) for item in intake_candidates]
c0_candidates = [item for item in diff_gate_candidates if item.get("control_tier") == "C0"]
c1_candidates = [item for item in diff_gate_candidates if item.get("control_tier") == "C1"]
return {
"schema_version": "public_gateway_rendered_diff_gate_draft_v1",
"generated_at": report_time,
"git_commit": git_short_sha(root),
"source_intake_preflight_schema_version": intake_report.get("schema_version"),
"source_intake_preflight_status": intake_report.get("status"),
"status": "rendered_diff_gate_draft_ready_no_runtime_action",
"summary": {
"diff_gate_candidate_count": len(diff_gate_candidates),
"c0_diff_gate_candidate_count": len(c0_candidates),
"c1_diff_gate_candidate_count": len(c1_candidates),
"diff_gate_field_count": len(DIFF_GATE_FIELDS),
"preflight_stage_count": len(PREFLIGHT_STAGES),
"blocked_action_count": len(BLOCKED_ACTIONS),
"redacted_export_accepted_count": 0,
"rendered_diff_candidate_count": 0,
"rendered_diff_ready_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,
"maintenance_window_accepted_count": 0,
"rollback_owner_accepted_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
},
"execution_boundaries": {
"read_live_conf_over_ssh": False,
"store_raw_live_conf": False,
"rendered_diff_authorized": 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_gate_fields": DIFF_GATE_FIELDS,
"preflight_stages": preflight_stages(),
"blocked_actions": BLOCKED_ACTIONS,
"diff_gate_candidates": diff_gate_candidates,
"next_steps": [
"等待 redacted export accepted metadata沒有 accepted metadata 前不得產生 rendered diff。",
"rendered diff candidate 必須另走 reviewer / owner review不得自動進 nginx -t。",
"`nginx -t`、reload、route smoke、DNS / TLS probe、certbot renew 與 production write 都必須另行人工批准。",
],
}
def main() -> int:
parser = argparse.ArgumentParser(description="IwoooS Public Gateway rendered diff gate 草稿產生器")
parser.add_argument("--root", default=".", help="repo root")
parser.add_argument(
"--intake-preflight-report",
default="docs/security/public-gateway-redacted-export-intake-preflight.snapshot.json",
help="public-gateway-redacted-export-intake-preflight.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()
intake_report = load_json(root / args.intake_preflight_report)
report = build_report(root, intake_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_GATE_DRAFT_OK "
f"candidates={summary['diff_gate_candidate_count']} "
f"c0={summary['c0_diff_gate_candidate_count']} "
f"stages={summary['preflight_stage_count']} "
f"blocked={summary['blocked_action_count']} "
f"runtime_gate={summary['runtime_gate_count']}",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
sys.exit(main())