327 lines
12 KiB
Python
327 lines
12 KiB
Python
#!/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())
|