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