#!/usr/bin/env python3 """ IwoooS Public Gateway redacted export 收件預檢產生器。 本工具讀取 Public Gateway live conf 匯出請求包,產生收到 owner-provided redacted export ref 之前的收件預檢與拒收規則。它不送件、不收 raw conf、 不保存 live conf、不產生 rendered 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)) INTAKE_FIELDS = [ "intake_id", "export_request_id", "config_id", "control_tier", "redacted_live_conf_ref", "ref_owner_role_or_team", "received_metadata_ref", "sensitive_payload_review", "quarantine_ref", "rendered_diff_candidate_ref", "not_approval", ] VALIDATION_CHECKS = [ ( "source_export_request_snapshot_current", "來源匯出請求 snapshot 必須是 committed 最新版本。", ), ( "request_sent_metadata_required", "沒有可稽核 request sent metadata 時,不得標記 received。", ), ( "recipient_confirmed_metadata_required", "沒有 owner / recipient role 確認時,不得標記 recipient confirmed。", ), ( "redacted_export_ref_required", "只接受脫敏 export ref;不得貼 raw live conf 內容。", ), ( "raw_live_conf_payload_blocked", "若內容含完整 live conf、私鑰、token、cookie、session 或 authorization header,必須拒收或隔離。", ), ( "redaction_policy_attestation_required", "owner 需聲明已依 redaction policy 處理 upstream credential、secret path 與敏感 payload。", ), ( "secret_marker_scan_required", "任何可存入 repo 的 metadata 必須先通過 secret marker / sensitive marker 檢查。", ), ( "scope_matches_export_request", "host、config_id、live path metadata 必須與匯出請求一致;不一致先補件。", ), ( "quarantine_before_storage", "疑似敏感 payload 只可留下 quarantine metadata,不得寫入 LOGBOOK、前端或 repo artifact。", ), ( "rendered_diff_gate_separate", "收件通過只代表可進 rendered diff preflight,不授權 nginx -t、reload 或 route smoke。", ), ] REJECTION_GUARDS = [ "raw_live_conf_payload", "tls_private_key", "token_secret_cookie_session", "authorization_header_or_basic_auth", "unredacted_upstream_credential", "db_url_or_env_dump", "shell_history_or_private_key", "unredacted_log_or_screenshot", "nginx_test_execution_request", "nginx_reload_or_route_change_request", "dns_tls_certbot_request", "ssh_host_write_or_runtime_request", ] REVIEWER_INTAKE_LANES = [ ( "keep_waiting_redacted_export", "尚未送件、尚未確認 recipient 或尚未收到脫敏 export ref 時維持等待。", "request_sent / received / accepted / rendered diff / runtime gate 全部維持 0。", ), ( "request_more_metadata", "缺 owner role、received metadata ref、scope 對應、redaction attestation 或 followup owner 時補件。", "不得保存 raw payload,不得增加 accepted。", ), ( "quarantine_sensitive_payload", "疑似含敏感 payload、raw conf、未脫敏 log 或 credential URL 時隔離。", "只可留下 quarantine metadata,不得寫入 repo、LOGBOOK 或前端。", ), ( "reject_execution_request", "夾帶 nginx -t、reload、route smoke、DNS / TLS、certbot、SSH 或 host write 要求時拒收。", "不得轉成 action button 或 runtime approval。", ), ( "ready_for_rendered_diff_preflight", "欄位完整、只有脫敏 ref、無敏感 payload 且無執行要求時,才可進 rendered diff preflight。", "仍不是 rendered diff ready,也不是 nginx / route / runtime 授權。", ), ] 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 validation_checks() -> list[dict[str, Any]]: return [ { "check_id": check_id, "status": "required_before_redacted_export_intake", "required": True, "instruction": instruction, "gate_effect": "不增加 received / accepted / rendered diff / runtime gate。", } for check_id, instruction in VALIDATION_CHECKS ] def reviewer_intake_lanes() -> list[dict[str, Any]]: return [ { "lane_id": lane_id, "status": "redacted_export_intake_lane_defined", "instruction": instruction, "gate_effect": gate_effect, } for lane_id, instruction, gate_effect in REVIEWER_INTAKE_LANES ] def intake_candidate_for(export_request: dict[str, Any]) -> dict[str, Any]: config_id = export_request["config_id"] return { "intake_id": f"public_gateway_redacted_export_intake:{config_id}", "status": "waiting_redacted_export_ref", "export_request_id": export_request["export_request_id"], "config_id": config_id, "host": export_request["host"], "live_path": export_request["live_path"], "role": export_request["role"], "control_tier": export_request["control_tier"], "owner_gate": export_request["owner_gate"], "source_snapshot_ref": "docs/security/public-gateway-live-conf-export-request.snapshot.json", "redaction_policy_ref": export_request["redaction_policy_ref"], "redacted_live_conf_ref": None, "ref_owner_role_or_team": "pending_owner_role_or_team", "received_metadata_ref": None, "sensitive_payload_review": "not_started", "quarantine_ref": None, "rendered_diff_candidate_ref": None, "intake_fields": INTAKE_FIELDS, "validation_checks": [check_id for check_id, _instruction in VALIDATION_CHECKS], "rejection_guards": REJECTION_GUARDS, "reviewer_intake_lanes": [lane_id for lane_id, _instruction, _effect in REVIEWER_INTAKE_LANES], "not_approval": True, "request_sent": False, "recipient_confirmed": False, "redacted_export_received": False, "accepted_redacted_export": False, "quarantine_written": False, "rejected_intake": False, "raw_live_conf_stored": False, "rendered_diff_candidate": False, "rendered_diff_authorized": False, "nginx_test_authorized": False, "nginx_test_executed": False, "nginx_reload_authorized": False, "route_smoke_authorized": False, "route_smoke_executed": False, "runtime_gate": False, "action_buttons_allowed": False, "secret_value_collection_allowed": False, "production_write_authorized": False, } def build_report(root: Path, export_request_report: dict[str, Any], generated_at: str | None) -> dict[str, Any]: report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds") export_requests = export_request_report.get("export_requests", []) candidates = [intake_candidate_for(item) for item in export_requests] c0_candidates = [item for item in candidates if item.get("control_tier") == "C0"] c1_candidates = [item for item in candidates if item.get("control_tier") == "C1"] return { "schema_version": "public_gateway_redacted_export_intake_preflight_v1", "generated_at": report_time, "git_commit": git_short_sha(root), "source_export_request_schema_version": export_request_report.get("schema_version"), "source_export_request_status": export_request_report.get("status"), "status": "redacted_export_intake_preflight_ready_no_payload_received", "summary": { "intake_candidate_count": len(candidates), "c0_intake_candidate_count": len(c0_candidates), "c1_intake_candidate_count": len(c1_candidates), "intake_field_count": len(INTAKE_FIELDS), "validation_check_count": len(VALIDATION_CHECKS), "rejection_guard_count": len(REJECTION_GUARDS), "reviewer_intake_lane_count": len(REVIEWER_INTAKE_LANES), "request_sent_count": 0, "recipient_confirmed_count": 0, "redacted_export_received_count": 0, "accepted_redacted_export_count": 0, "quarantined_payload_count": 0, "rejected_intake_count": 0, "rendered_diff_candidate_count": 0, "raw_live_conf_stored_count": 0, "nginx_test_authorized_count": 0, "nginx_test_executed_count": 0, "nginx_reload_authorized_count": 0, "route_smoke_authorized_count": 0, "route_smoke_executed_count": 0, "runtime_gate_count": 0, "action_button_count": 0, }, "execution_boundaries": { "request_sent": False, "recipient_confirmed": False, "redacted_export_received": False, "raw_live_conf_storage_allowed": 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_query_executed": False, "live_tls_probe_executed": False, "certbot_renew_authorized": False, "ssh_read_authorized": False, "host_live_conf_read_authorized": False, "runtime_execution_authorized": False, "action_buttons_allowed": False, "secret_value_collection_allowed": False, "production_write_authorized": False, "not_authorization": True, }, "intake_fields": INTAKE_FIELDS, "validation_checks": validation_checks(), "rejection_guards": REJECTION_GUARDS, "reviewer_intake_lanes": reviewer_intake_lanes(), "intake_candidates": candidates, "next_steps": [ "只有收到可稽核 request sent metadata 與 owner-provided redacted export ref 後,才能另建收件紀錄。", "任何疑似 raw conf 或 sensitive payload 必須先 quarantine,不得寫入 repo、LOGBOOK 或前端。", "收件通過後仍只能進 rendered diff preflight;nginx -t、reload、route smoke 需另行人工批准。", ], } def main() -> int: parser = argparse.ArgumentParser(description="IwoooS Public Gateway redacted export 收件預檢產生器") parser.add_argument("--root", default=".", help="repo root") parser.add_argument( "--export-request-report", default="docs/security/public-gateway-live-conf-export-request.snapshot.json", help="public-gateway-live-conf-export-request.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() export_request_report = load_json(root / args.export_request_report) report = build_report(root, export_request_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_REDACTED_EXPORT_INTAKE_PREFLIGHT_OK " f"candidates={summary['intake_candidate_count']} " f"c0={summary['c0_intake_candidate_count']} " f"checks={summary['validation_check_count']} " f"rejected={summary['rejected_intake_count']} " f"runtime_gate={summary['runtime_gate_count']}", file=sys.stderr, ) return 0 if __name__ == "__main__": sys.exit(main())