Files
awoooi/scripts/security/high-value-config-owner-packet.py

263 lines
9.6 KiB
Python
Raw Permalink 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 高價值配置 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 完成後才可進 acceptedaccepted 仍不開 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())