#!/usr/bin/env python3 """ IwoooS 高價值配置 owner response packet 產生器。 本工具讀取 high-value-config-change-gate 的 JSON 報告,為 impacted categories 產出 owner response packet 草案。它只產生欄位、補件規則與 禁止事項,不送件、不收回覆、不建立 action button、不執行 runtime。 """ 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)) CANONICAL_OWNER_FIELDS = [ "owner_role_or_team", "decision", "decision_reason", "affected_scope", "redacted_evidence_refs", "followup_owner", "rollback_owner", "maintenance_window", "validation_plan", ] ALLOWED_DECISIONS = ["confirm", "defer", "reject", "request_more_evidence"] FIELD_INSTRUCTIONS = { "owner_role_or_team": "填角色或團隊,不填私人帳號、密碼、token 或私人聯絡資訊。", "decision": "只能填 confirm、defer、reject、request_more_evidence;不得附帶 runtime 執行批准。", "decision_reason": "填脫敏短理由;不可貼 raw log、raw API body、未脫敏截圖或內部對話。", "affected_scope": "填受影響 repo、host、domain、namespace、route、service、secret name 或產品邊界。", "redacted_evidence_refs": "只填文件路徑、snapshot id、ticket id、commit、hash 或脫敏 metadata pointer。", "followup_owner": "填後續補證、審查或決策負責角色 / 團隊。", "rollback_owner": "填回滾負責角色 / 團隊;不是直接執行授權。", "maintenance_window": "填維護窗口或明確寫 deferred / not scheduled;不得用口頭同意代替。", "validation_plan": "填 preflight、post-check、rollback check;若只讀文件變更,寫 guard / json / doc secret sanity。", } FALSE_FLAGS = [ "request_sent", "response_received", "response_accepted", "runtime_execution_authorized", "host_write_authorized", "nginx_reload_authorized", "dns_tls_change_authorized", "workflow_modification_authorized", "runner_change_authorized", "refs_sync_authorized", "force_push_authorized", "secret_value_collection_allowed", "active_scan_authorized", "action_buttons_allowed", ] BLOCKED_REQUESTS = [ "repo_create", "visibility_change", "refs_sync", "refs_delete", "force_push", "workflow_modify", "runner_enable", "secret_value_submit", "ssh_host_modify", "nginx_reload", "dns_tls_modify", "argocd_sync", "kubectl_apply", "active_scan", "agent_bounty_runtime_execute", "payout_or_withdrawal", ] OUTCOME_LANES = [ "keep_waiting_owner_response", "request_more_evidence", "quarantine_sensitive_payload", "reject_execution_request", "ready_for_reviewer_validation", ] 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 field_templates() -> list[dict[str, Any]]: return [ { "field": field, "required": True, "instruction": FIELD_INSTRUCTIONS[field], } for field in CANONICAL_OWNER_FIELDS ] def false_flag_map() -> dict[str, bool]: return {flag: False for flag in FALSE_FLAGS} def files_for_category(gate_report: dict[str, Any], category_id: str) -> list[str]: paths: list[str] = [] for item in gate_report.get("changed_files", []): if any(category.get("category_id") == category_id for category in item.get("categories", [])): paths.append(item["path"]) return sorted(paths) def category_inventory_by_id(gate_report: dict[str, Any]) -> dict[str, dict[str, Any]]: return { item["category_id"]: item for item in gate_report.get("control_category_inventory", []) } def packet_for_category(gate_report: dict[str, Any], category: dict[str, Any]) -> dict[str, Any]: inventory = category_inventory_by_id(gate_report).get(category["category_id"], {}) affected_files = files_for_category(gate_report, category["category_id"]) return { "packet_id": f"high_value_config_owner_packet:{category['category_id']}", "status": "draft_waiting_owner_response", "category_id": category["category_id"], "label": category["label"], "priority": category["priority"], "control_tier": category["control_tier"], "required_gate": category["required_gate"], "affected_files": affected_files, "allowed_decisions": ALLOWED_DECISIONS, "field_templates": field_templates(), "required_validation": category.get("required_validation") or inventory.get("required_validation", []), "redaction_rules": [ "只收 redacted evidence refs,不收 secret value。", "疑似 token、cookie、authorization header、private key、runner token 或 webhook secret 一律 quarantine。", "內部工作視窗對話、抱怨、口頭同意不得進產品文案或 LOGBOOK raw text。", ], "blocked_requests": BLOCKED_REQUESTS, "outcome_lanes": OUTCOME_LANES, "false_flags": false_flag_map(), "reviewer_checklist": [ "canonical owner fields 全部存在。", "decision 只使用允許值。", "affected scope 可映射到 repo / host / domain / route / service / secret name。", "redacted evidence refs 不含 raw payload。", "沒有夾帶執行要求。", "C0 / C1 若要進 runtime,需獨立人工批准與維護窗口。", ], } def build_report(root: Path, gate_report: dict[str, Any], generated_at: str | None) -> dict[str, Any]: report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds") impacted_categories = gate_report.get("impacted_categories", []) packets = [packet_for_category(gate_report, category) for category in impacted_categories] c0_packets = [packet for packet in packets if packet["control_tier"] == "C0"] c1_packets = [packet for packet in packets if packet["control_tier"] == "C1"] return { "schema_version": "high_value_config_owner_packet_v1", "generated_at": report_time, "git_commit": git_short_sha(root), "source_gate_schema_version": gate_report.get("schema_version"), "source_gate_summary": gate_report.get("summary", {}), "status": "draft_waiting_owner_response", "canonical_owner_fields": CANONICAL_OWNER_FIELDS, "allowed_decisions": ALLOWED_DECISIONS, "execution_boundaries": { "request_sent": False, "response_received": False, "response_accepted": False, "runtime_execution_authorized": False, "host_write_authorized": False, "secret_value_collected": False, "action_buttons_allowed": False, }, "summary": { "packet_count": len(packets), "c0_packet_count": len(c0_packets), "c1_packet_count": len(c1_packets), "request_sent_count": 0, "received_response_count": 0, "accepted_response_count": 0, "runtime_gate_count": 0, }, "universal_owner_response_template": { "allowed_decisions": ALLOWED_DECISIONS, "field_templates": field_templates(), "false_flags": false_flag_map(), }, "packets": packets, "control_category_inventory": gate_report.get("control_category_inventory", []), "next_steps": [ "若 packet_count > 0,將 packet 交給 owner 補 canonical 欄位;不得把草案視為已送件。", "若 owner 回覆含 secret 或執行要求,先 quarantine 或 reject_execution_request。", "只有 reviewer checklist 完成後才可進 accepted;accepted 仍不開 runtime gate。", ], } def main() -> int: parser = argparse.ArgumentParser(description="IwoooS 高價值配置 owner response packet 產生器") parser.add_argument("--root", default=".", help="repo root") parser.add_argument( "--gate-report", default="docs/security/high-value-config-change-gate.snapshot.json", help="high-value-config-change-gate.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() gate_report = load_json(root / args.gate_report) report = build_report(root, gate_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( "HIGH_VALUE_CONFIG_OWNER_PACKET_OK " f"packets={summary['packet_count']} " f"c0={summary['c0_packet_count']} " f"c1={summary['c1_packet_count']} " f"runtime_gate={summary['runtime_gate_count']}", file=sys.stderr, ) return 0 if __name__ == "__main__": sys.exit(main())