Files
awoooi/scripts/security/source-control-approval-board.py

259 lines
9.7 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
"""產生 Gitea -> GitHub 逐 repo approval board。
此工具只讀取既有 redacted snapshot不呼叫 Gitea/GitHub API不需要 token。
用途是讓 AwoooP / PR reviewer 可以看見每個 repo 的下一個低摩擦決策點。
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
def load_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def infer_lane(recommended_action: str) -> str:
if recommended_action == "hold_refs_reconcile":
return "refs_reconcile"
if recommended_action == "confirm_internal_remote_purpose":
return "internal_remote_purpose"
if recommended_action == "scope_review_only":
return "scope_review"
return "target_creation_or_access"
def required_decision(lane: str) -> str:
mapping = {
"refs_reconcile": "決定 Gitea / GitHub refs 真相來源,並批准只產生 reconcile plan。",
"target_creation_or_access": "決定 GitHub repo owner / visibility / 是否建立或授權既有 repo。",
"internal_remote_purpose": "決定 110 internal remote 是 active source、legacy mirror 或應降級。",
"scope_review": "決定此 repo 是否屬於 AWOOOI 資安供應鏈範圍。",
}
return mapping[lane]
def low_friction_next_step(lane: str) -> str:
mapping = {
"refs_reconcile": "先產生 draft reconcile plan不 push refs、不切 primary。",
"target_creation_or_access": "先取得 owner / visibility 決策,不自動建立 repo。",
"internal_remote_purpose": "先文件化用途與風險,不刪除 remote、不同步 refs。",
"scope_review": "只標記 scope review不納入主控切換。",
}
return mapping[lane]
def awooop_consumption(lane: str, approval_required: bool) -> str:
if lane == "scope_review":
return "scope_review_only"
if approval_required:
return "approval_candidate"
return "mirror_only"
def build_board(args: argparse.Namespace) -> dict[str, Any]:
decisions = load_json(Path(args.github_target_decision))
packages = load_json(Path(args.repo_approval_package))
gitea_inventory = load_json(Path(args.gitea_inventory))
package_by_repo = {
str(item.get("github_repo", "")): item
for item in packages.get("approval_items", [])
if isinstance(item, dict)
}
board_items: list[dict[str, Any]] = []
pending_count = 0
for decision in decisions.get("decisions", []):
if not isinstance(decision, dict):
continue
github_repo = str(decision.get("github_repo", ""))
package = package_by_repo.get(github_repo, {})
approval_required = bool(decision.get("approval_required", False))
if approval_required:
pending_count += 1
lane = infer_lane(str(decision.get("recommended_action", "")))
approval_status = str(package.get("approval_status") or ("pending" if approval_required else "not_required"))
blocked_until = package.get("blocked_until") or decision.get("blocked_until") or []
evidence_refs = sorted(
{
*[str(value) for value in decision.get("evidence_refs", [])],
*[str(value) for value in package.get("evidence_refs", [])],
}
)
board_items.append(
{
"github_repo": github_repo,
"source_key": str(decision.get("source_key", "")),
"lane": lane,
"risk": str(decision.get("risk", "LOW")),
"probe_status": str(decision.get("probe_status", "")),
"target_state": str(decision.get("target_state", "")),
"approval_status": approval_status,
"required_decision": required_decision(lane),
"low_friction_next_step": low_friction_next_step(lane),
"blocked_until": [str(value) for value in blocked_until],
"allowed_after_approval": [
str(value)
for value in (package.get("allowed_after_approval") or ["mirror_decision_only"])
],
"still_forbidden": [
str(value)
for value in (
package.get("still_forbidden")
or ["auto_execute", "sync_refs", "switch_primary"]
)
],
"evidence_refs": evidence_refs,
"awooop_consumption": awooop_consumption(lane, approval_required),
}
)
inventory_status = str(gitea_inventory.get("status", "blocked"))
gate_status = "ready" if inventory_status == "ok" else "blocked"
gate_reason = (
"Gitea authenticated 或 admin_export inventory 已完成。"
if gate_status == "ready"
else "GITEA_READONLY_TOKEN 未提供,且不使用可 push 的既有 remote credential 當 read-only tokenserver-side private/internal repo list 仍未完成。"
)
return {
"schema_version": "source_control_approval_board_v1",
"status": "draft",
"date": args.date,
"default_mode": "mirror_only",
"authenticated_inventory_gate": {
"status": gate_status,
"reason": gate_reason,
"allowed_next_step": [
"提供 read-only token 後重跑 gitea-repo-inventory",
"或提供 redacted admin export JSON",
"在 gate 前仍可維護 approval board 與 decision table",
],
"still_forbidden": [
"使用 write-capable credential 當作 read-only token",
"建立 GitHub repo",
"修改 repo visibility",
"sync refs",
"switch GitHub primary",
],
},
"item_count": len(board_items),
"pending_approval_count": pending_count,
"board_items": board_items,
}
def write_markdown(board: dict[str, Any], path: Path) -> None:
gate = board["authenticated_inventory_gate"]
lines = [
"# Source Control Approval Board",
"",
"| 項目 | 內容 |",
"|------|------|",
f"| 日期 | {board['date']} |",
f"| 狀態 | `{board['status']}` |",
f"| 預設模式 | `{board['default_mode']}` |",
f"| authenticated inventory gate | `{gate['status']}` |",
f"| gate 原因 | {gate['reason']} |",
f"| repo items | {board['item_count']} |",
f"| pending approval | {board['pending_approval_count']} |",
"",
"## 0. 核心原則",
"",
"本 board 只整理決策不授權執行。AwoooP 可以 mirror 成 approval candidate但不得建立 repo、修改 visibility、同步 refs、切 GitHub primary 或保存 credential value。",
"",
"## 1. 逐 repo 決策隊列",
"",
"| GitHub repo | Lane | Risk | Probe | Approval | 下一步 |",
"|-------------|------|------|-------|----------|--------|",
]
for item in board["board_items"]:
lines.append(
"| "
+ " | ".join(
[
f"`{item['github_repo']}`",
f"`{item['lane']}`",
f"`{item['risk']}`",
f"`{item['probe_status']}`",
f"`{item['approval_status']}`",
item["low_friction_next_step"],
]
)
+ " |"
)
lines.extend(["", "## 2. 詳細阻塞點", ""])
for item in board["board_items"]:
lines.extend(
[
f"### {item['github_repo']}",
"",
f"- Source key`{item['source_key']}`",
f"- Required decision{item['required_decision']}",
f"- AwoooP consumption`{item['awooop_consumption']}`",
"- Blocked until",
]
)
for value in item["blocked_until"]:
lines.append(f" - {value}")
lines.append("- Still forbidden")
for value in item["still_forbidden"]:
lines.append(f" - {value}")
lines.append("- Evidence refs")
for value in item["evidence_refs"]:
lines.append(f" - `{value}`")
lines.append("")
lines.extend(
[
"## 3. Gate 前允許做的事",
"",
"1. 更新 read-only evidence。",
"2. 更新 approval board / decision table。",
"3. 寫 draft reconcile plan。",
"4. 把 pending approval mirror 到 AwoooP。",
"",
"## 4. Gate 前仍禁止",
"",
]
)
for value in gate["still_forbidden"]:
lines.append(f"- {value}")
lines.append("")
path.write_text("\n".join(lines), encoding="utf-8")
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--date", required=True)
parser.add_argument("--github-target-decision", default="docs/security/github-target-decision.snapshot.json")
parser.add_argument(
"--repo-approval-package",
default="docs/security/github-target-repo-approval-package.snapshot.json",
)
parser.add_argument("--gitea-inventory", default="docs/security/gitea-repo-inventory.snapshot.json")
parser.add_argument("--output-json", required=True)
parser.add_argument("--output-md", required=True)
args = parser.parse_args()
board = build_board(args)
Path(args.output_json).write_text(
json.dumps(board, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
write_markdown(board, Path(args.output_md))
print(f"OK source-control approval board items={board['item_count']}")
return 0
if __name__ == "__main__":
raise SystemExit(main())