Files
awoooi/scripts/security/public-gateway-redacted-export-intake-preflight.py

327 lines
12 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 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 preflightnginx -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())