288 lines
11 KiB
Python
288 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
IwoooS 高價值配置 owner packet 收件預檢產生器。
|
||
|
||
本工具讀取 committed owner packet snapshot,產生 request dispatch 前的
|
||
只讀預檢與 reviewer intake lanes。它不送 request、不收回覆、不寫入
|
||
reviewer queue、不建立 action button,也不開啟 runtime gate。
|
||
"""
|
||
|
||
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))
|
||
|
||
DISPATCH_PREFLIGHT_CHECKS = [
|
||
(
|
||
"baseline_commit_synced",
|
||
"基線 commit 已同步",
|
||
"使用最新 gitea/main 與 owner packet snapshot;不得用過期 packet 送件。",
|
||
),
|
||
(
|
||
"source_snapshot_current",
|
||
"來源 snapshot 目前有效",
|
||
"來源必須是 committed high-value-config-owner-packet snapshot,不讀 live 主機。",
|
||
),
|
||
(
|
||
"packet_scope_mapped",
|
||
"Packet scope 已映射",
|
||
"每包需有 category、priority、control tier、required gate 與 affected files。",
|
||
),
|
||
(
|
||
"canonical_fields_present",
|
||
"Canonical 欄位已固定",
|
||
"每包需要求 owner role、decision、reason、scope、evidence refs、followup、rollback、window、validation。",
|
||
),
|
||
(
|
||
"redacted_evidence_refs_only",
|
||
"Evidence refs 只允許脫敏參照",
|
||
"只收文件路徑、snapshot id、ticket id、commit、hash 或 metadata pointer。",
|
||
),
|
||
(
|
||
"no_sensitive_payload",
|
||
"禁止敏感 payload",
|
||
"不得收 token、secret、private key、cookie、session、authorization header、runner token 或 webhook secret。",
|
||
),
|
||
(
|
||
"no_execution_request",
|
||
"拒收執行要求",
|
||
"不得夾帶 reload、deploy、sync、host write、active scan、payout 或 withdrawal。",
|
||
),
|
||
(
|
||
"maintenance_and_rollback_fields_present",
|
||
"維護窗口與回滾 owner 欄位存在",
|
||
"C0 / P0 仍需獨立人工批准、維護窗口與 rollback owner,不能由 packet 自動授權。",
|
||
),
|
||
(
|
||
"validation_plan_present",
|
||
"驗證計畫欄位存在",
|
||
"必須能說明 preflight、post-check、rollback check;只讀文件變更則對應 guard / JSON / doc sanity。",
|
||
),
|
||
]
|
||
|
||
REVIEWER_INTAKE_LANES = [
|
||
(
|
||
"keep_waiting_owner_response",
|
||
"尚未收到 owner response 或只有空白 / 口頭同意時維持等待。",
|
||
"request / received / accepted / rejected 維持 0。",
|
||
),
|
||
(
|
||
"request_more_evidence",
|
||
"欄位缺漏、scope 不清或 redacted evidence refs 不足時補件。",
|
||
"不得增加 accepted 或 runtime gate。",
|
||
),
|
||
(
|
||
"quarantine_sensitive_payload",
|
||
"疑似含敏感 payload、raw log、未脫敏截圖或 private credential URL 時隔離。",
|
||
"不得保存 raw payload、不得渲染到前端或 LOGBOOK。",
|
||
),
|
||
(
|
||
"reject_execution_request",
|
||
"夾帶 repo / refs / workflow / runner / host / scan / runtime 執行要求時拒收。",
|
||
"不得建立 action button、不得轉成執行批准。",
|
||
),
|
||
(
|
||
"ready_for_reviewer_validation",
|
||
"欄位完整、證據脫敏、無敏感 payload 且無執行要求時才可進 reviewer checklist。",
|
||
"仍不是 accepted,也不是 runtime authorization。",
|
||
),
|
||
]
|
||
|
||
NEXT_STEPS = [
|
||
"先由 reviewer 確認預檢 snapshot 與 owner packet snapshot 皆為最新 committed evidence。",
|
||
"送件前只準備脫敏欄位與禁止條款,不送 secret value、不附 raw payload。",
|
||
"收到回覆後先依 intake lanes 分流;通過 reviewer validation 之前 received / accepted 不得增加。",
|
||
"任何 runtime、Nginx reload、DNS / TLS、workflow、runner、host 或 payout 動作都必須切獨立人工批准。",
|
||
]
|
||
|
||
|
||
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 dispatch_preflight_checks() -> list[dict[str, Any]]:
|
||
return [
|
||
{
|
||
"check_id": check_id,
|
||
"label": label,
|
||
"status": "preflight_required_before_request_dispatch",
|
||
"required": True,
|
||
"instruction": instruction,
|
||
"gate_effect": "不增加 request_sent / received / accepted / runtime gate。",
|
||
}
|
||
for check_id, label, instruction in DISPATCH_PREFLIGHT_CHECKS
|
||
]
|
||
|
||
|
||
def reviewer_intake_lanes() -> list[dict[str, Any]]:
|
||
return [
|
||
{
|
||
"lane_id": lane_id,
|
||
"status": "reviewer_intake_lane_defined",
|
||
"instruction": instruction,
|
||
"gate_effect": gate_effect,
|
||
}
|
||
for lane_id, instruction, gate_effect in REVIEWER_INTAKE_LANES
|
||
]
|
||
|
||
|
||
def intake_packet(packet: dict[str, Any]) -> dict[str, Any]:
|
||
required_owner_fields = [
|
||
field["field"]
|
||
for field in packet.get("field_templates", [])
|
||
if field.get("required") is True
|
||
]
|
||
return {
|
||
"packet_id": packet["packet_id"],
|
||
"status": "waiting_request_dispatch_preflight",
|
||
"category_id": packet["category_id"],
|
||
"label": packet["label"],
|
||
"priority": packet["priority"],
|
||
"control_tier": packet["control_tier"],
|
||
"required_gate": packet["required_gate"],
|
||
"affected_file_count": len(packet.get("affected_files", [])),
|
||
"affected_files": packet.get("affected_files", []),
|
||
"required_owner_fields": required_owner_fields,
|
||
"required_validation": packet.get("required_validation", []),
|
||
"request_sent": False,
|
||
"received_response": False,
|
||
"accepted_response": False,
|
||
"rejected_response": False,
|
||
"reviewer_queue_write": False,
|
||
"runtime_gate": False,
|
||
"action_buttons_allowed": False,
|
||
"secret_value_collection_allowed": False,
|
||
"production_write_authorized": False,
|
||
"not_authorization": True,
|
||
}
|
||
|
||
|
||
def build_report(root: Path, owner_packet_report: dict[str, Any], generated_at: str | None) -> dict[str, Any]:
|
||
report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
|
||
packets = owner_packet_report.get("packets", [])
|
||
c0_packets = [packet for packet in packets if packet.get("control_tier") == "C0"]
|
||
c1_packets = [packet for packet in packets if packet.get("control_tier") == "C1"]
|
||
canonical_owner_fields = owner_packet_report.get("canonical_owner_fields", [])
|
||
required_owner_field_total = len(canonical_owner_fields) * len(packets)
|
||
blocked_requests = packets[0].get("blocked_requests", []) if packets else []
|
||
false_flags = owner_packet_report.get("universal_owner_response_template", {}).get("false_flags", {})
|
||
|
||
return {
|
||
"schema_version": "high_value_config_owner_packet_intake_preflight_v1",
|
||
"generated_at": report_time,
|
||
"git_commit": git_short_sha(root),
|
||
"source_owner_packet_schema_version": owner_packet_report.get("schema_version"),
|
||
"source_owner_packet_status": owner_packet_report.get("status"),
|
||
"status": "request_dispatch_preflight_ready",
|
||
"summary": {
|
||
"packet_count": len(packets),
|
||
"c0_packet_count": len(c0_packets),
|
||
"c1_packet_count": len(c1_packets),
|
||
"canonical_owner_field_count": len(canonical_owner_fields),
|
||
"required_owner_field_total": required_owner_field_total,
|
||
"dispatch_preflight_check_count": len(DISPATCH_PREFLIGHT_CHECKS),
|
||
"reviewer_intake_lane_count": len(REVIEWER_INTAKE_LANES),
|
||
"blocked_request_count": len(blocked_requests),
|
||
"request_sent_count": 0,
|
||
"received_response_count": 0,
|
||
"accepted_response_count": 0,
|
||
"rejected_response_count": 0,
|
||
"reviewer_queue_write_count": 0,
|
||
"runtime_gate_count": 0,
|
||
"action_button_count": 0,
|
||
},
|
||
"execution_boundaries": {
|
||
"dispatch_authorized": False,
|
||
"request_sent": False,
|
||
"reviewer_queue_write": False,
|
||
"runtime_execution_authorized": False,
|
||
"secret_value_collection_allowed": False,
|
||
"production_write_authorized": False,
|
||
"host_write_authorized": False,
|
||
"nginx_reload_authorized": False,
|
||
"dns_tls_change_authorized": False,
|
||
"workflow_modification_authorized": False,
|
||
"active_scan_authorized": False,
|
||
"action_buttons_allowed": False,
|
||
"not_authorization": True,
|
||
},
|
||
"canonical_owner_fields": canonical_owner_fields,
|
||
"allowed_decisions": owner_packet_report.get("allowed_decisions", []),
|
||
"dispatch_preflight_checks": dispatch_preflight_checks(),
|
||
"reviewer_intake_lanes": reviewer_intake_lanes(),
|
||
"blocked_requests": blocked_requests,
|
||
"source_false_flags": false_flags,
|
||
"intake_packets": [intake_packet(packet) for packet in packets],
|
||
"completion": {
|
||
"request_dispatch_preflight_artifact_percent": 100,
|
||
"owner_request_sent_percent": 0,
|
||
"owner_response_received_percent": 0,
|
||
"reviewer_accepted_percent": 0,
|
||
"runtime_gate_percent": 0,
|
||
},
|
||
"next_steps": NEXT_STEPS,
|
||
}
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="IwoooS 高價值配置 owner packet 收件預檢產生器")
|
||
parser.add_argument("--root", default=".", help="repo root")
|
||
parser.add_argument(
|
||
"--owner-packet-report",
|
||
default="docs/security/high-value-config-owner-packet.snapshot.json",
|
||
help="high-value-config-owner-packet.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()
|
||
owner_packet_report = load_json(root / args.owner_packet_report)
|
||
report = build_report(root, owner_packet_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_INTAKE_PREFLIGHT_OK "
|
||
f"packets={summary['packet_count']} "
|
||
f"c0={summary['c0_packet_count']} "
|
||
f"checks={summary['dispatch_preflight_check_count']} "
|
||
f"lanes={summary['reviewer_intake_lane_count']} "
|
||
f"runtime_gate={summary['runtime_gate_count']}",
|
||
file=sys.stderr,
|
||
)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|