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

288 lines
11 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
"""
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())