Files
awoooi/scripts/security/host-service-owner-response-acceptance.py
Your Name 41f5ff1a38
All checks were successful
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m40s
CD Pipeline / build-and-deploy (push) Successful in 6m17s
CD Pipeline / post-deploy-checks (push) Successful in 1m35s
feat(iwooos): 強化主機服務事故回補 gate
2026-06-15 14:51:25 +08:00

376 lines
18 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 Docker / systemd / host service owner response acceptance 只讀帳本產生器。
本工具讀取 host service repo-only 清冊與 owner request draft建立未來
owner response 如何收件、補件、隔離、拒收或進 host service review 的
metadata-only acceptance ledger。它不 SSH、不讀 live host、不執行 docker
compose、不執行 systemctl、不呼叫 repair-bot、不跑 Ansible、不收 secret
value、不寫 production host。
"""
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))
ACCEPTANCE_FIELDS = [
"acceptance_candidate_id",
"request_id",
"surface_id",
"label",
"expected_host_scope",
"config_kind",
"service_scope",
"control_tier",
"repo_source_path",
"repo_sha256",
"source_line_count",
"write_capable_surface",
"requires_live_evidence",
"owner_response_ref",
"owner_role_or_team",
"decision",
"decision_reason",
"affected_scope",
"redacted_evidence_refs",
"live_config_hash_ref",
"maintenance_window",
"restart_window",
"rollback_owner",
"post_check_plan",
"disable_switch",
"config_source_of_truth_ref",
"service_dependency_map_ref",
"port_binding_inventory_ref",
"cold_start_sequence_ref",
"incident_recovery_evidence_ref",
"daemon_runner_contention_ref",
"reviewer_outcome",
"followup_owner",
"not_approval",
]
INCIDENT_OWNER_RESPONSE_FIELDS = [
"config_source_of_truth_ref",
"service_dependency_map_ref",
"port_binding_inventory_ref",
"cold_start_sequence_ref",
"incident_recovery_evidence_ref",
"daemon_runner_contention_ref",
]
REVIEWER_CHECKS = [
{"check_id": "owner_identity_present", "instruction": "owner role / team 必須可追溯。"},
{"check_id": "decision_reason_present", "instruction": "decision 與 decision reason 必須同時存在。"},
{"check_id": "affected_scope_matches_surface", "instruction": "affected scope 必須能對回 committed surface_id。"},
{"check_id": "redacted_refs_only", "instruction": "evidence 只能是脫敏 ref、hash、ticket、commit 或 artifact pointer。"},
{"check_id": "secret_value_absent", "instruction": "不得出現 token、password、cookie、private key、env dump 或 partial secret。"},
{"check_id": "live_config_hash_metadata_only", "instruction": "live config hash 只能是 owner-provided metadata ref不得貼 raw live config。"},
{"check_id": "maintenance_window_present", "instruction": "未來 host read、restart、repair-bot 或 Ansible 動作需獨立維護窗口。"},
{"check_id": "restart_window_separate_from_action", "instruction": "restart window 與 docker / systemctl action 必須分離,不得自動執行。"},
{"check_id": "rollback_owner_present", "instruction": "rollback owner、rollback ref 或 disable path 必須存在。"},
{"check_id": "post_check_plan_present", "instruction": "post-check 必須列服務健康、route、queue、log 與 rollback 停止條件。"},
{"check_id": "disable_switch_present", "instruction": "repair-bot、Ansible role 或 service config 需有 disable switch 或 freeze rule。"},
{"check_id": "config_source_of_truth_present", "instruction": "必須提供 repo source、live source、runner source 與 backup source 的真相來源 ref不得只用口頭描述。"},
{"check_id": "service_dependency_map_present", "instruction": "必須提供服務依賴 ref涵蓋上游、下游、資料庫、queue、registry、AI provider 與 public route 影響。"},
{"check_id": "port_binding_inventory_present", "instruction": "必須提供 port binding / exposure inventory ref避免 host port、container port、proxy 與防火牆狀態彼此漂移。"},
{"check_id": "cold_start_sequence_present", "instruction": "必須提供 cold-start / recovery sequence ref明確列出 Docker daemon、compose stack、systemd unit、runner 與 post-check 順序。"},
{"check_id": "incident_recovery_evidence_present", "instruction": "若回覆涉及服務異常、重啟或端口事故必須提供恢復時間、服務健康、route health 與 operator notice ref。"},
{"check_id": "daemon_runner_contention_reviewed", "instruction": "必須說明 Docker daemon、iptables / xtables、runner、repair-bot、backup job 或 compose action 是否可能互相競爭。"},
{"check_id": "silent_restart_not_accepted", "instruction": "不得接受沒有 actor、原因、依賴圖、port inventory、回滾與 post-check 的靜默 restart / reload。"},
{"check_id": "write_capable_requires_extra_review", "instruction": "write-capable surface 必須進額外 reviewer review不得直接 accepted。"},
{"check_id": "no_runtime_request", "instruction": "夾帶 SSH、Docker、systemctl、repair-bot、Ansible、sudo 或 host write 要求時拒收。"},
{"check_id": "counts_transition_safe", "instruction": "只有 reviewer record 可更新 received / accepted / rejected不得同時開 runtime gate。"},
]
OUTCOME_LANES = [
{"lane_id": "waiting_owner_response", "meaning": "尚未收到 owner response所有 accepted / runtime count 維持 0。"},
{"lane_id": "quarantine_secret_or_raw_payload", "meaning": "收到 secret、env dump、raw compose、raw systemd unit 或未脫敏 host config 時隔離。"},
{"lane_id": "reject_execution_request", "meaning": "夾帶 SSH、docker compose、systemctl、repair-bot、Ansible、sudo 或 host write 要求時拒收。"},
{"lane_id": "request_supplement", "meaning": "欄位不足、scope 不清、live hash ref / rollback / post-check 缺失時要求補件。"},
{"lane_id": "incident_recovery_backfill_required", "meaning": "涉及服務異常、靜默重啟、端口事故或 cold-start recovery 時,必須進事故回補,不得直接 accepted。"},
{"lane_id": "ready_for_host_service_review", "meaning": "metadata 合格後,只能進 host service reviewer review。"},
{"lane_id": "owner_review_only_update", "meaning": "只允許更新只讀 owner review ledger不得改 compose、systemd、repair-bot 或 Ansible。"},
{"lane_id": "waiting_runtime_gate", "meaning": "即使 owner response acceptedruntime gate 仍等待獨立人工批准。"},
]
BLOCKED_ACTIONS = [
"ssh_read",
"ssh_write",
"docker_compose_up",
"docker_compose_down",
"docker_compose_pull",
"systemctl_restart",
"systemctl_reload",
"repair_bot_execute",
"ansible_apply",
"sudo_action",
"host_file_write",
"firewall_change",
"secret_value_collection",
"active_scan",
"live_host_read",
"raw_live_config_storage",
"restart_without_window",
"rollback_without_owner",
"accept_silent_restart",
"treat_service_healthy_as_config_accepted",
"skip_config_source_of_truth_review",
"skip_service_dependency_map",
"skip_port_binding_review",
"skip_cold_start_sequence",
"hide_daemon_runner_contention",
"runtime_gate_open",
"add_action_button",
]
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 acceptance_candidate(request: dict[str, Any]) -> dict[str, Any]:
surface_id = request["surface_id"]
return {
"acceptance_candidate_id": f"host_service_owner_response_acceptance:{surface_id}",
"status": "waiting_owner_response",
"request_id": request["request_id"],
"surface_id": surface_id,
"label": request["label"],
"expected_host_scope": request["expected_host_scope"],
"config_kind": request["config_kind"],
"service_scope": request["service_scope"],
"control_tier": request["control_tier"],
"repo_source_path": request["repo_source_path"],
"repo_sha256": request["repo_sha256"],
"source_line_count": request["source_line_count"],
"write_capable_surface": request["write_capable_surface"],
"requires_live_evidence": request["requires_live_evidence"],
"owner_response_ref": None,
"owner_role_or_team": "pending_owner_response",
"decision": "pending_owner_response",
"decision_reason": "pending_owner_response",
"affected_scope": "pending_owner_response",
"redacted_evidence_refs": [],
"live_config_hash_ref": None,
"maintenance_window": "pending_owner_response",
"restart_window": "pending_owner_response",
"rollback_owner": "pending_owner_response",
"post_check_plan": "pending_owner_response",
"disable_switch": "pending_owner_response",
"config_source_of_truth_ref": None,
"service_dependency_map_ref": None,
"port_binding_inventory_ref": None,
"cold_start_sequence_ref": None,
"incident_recovery_evidence_ref": None,
"daemon_runner_contention_ref": None,
"reviewer_outcome": "waiting_owner_response",
"followup_owner": "pending_owner_response",
"acceptance_fields": ACCEPTANCE_FIELDS,
"required_owner_fields": [*request["required_owner_fields"], *INCIDENT_OWNER_RESPONSE_FIELDS],
"reviewer_checks": [item["check_id"] for item in REVIEWER_CHECKS],
"outcome_lanes": [item["lane_id"] for item in OUTCOME_LANES],
"blocked_actions": BLOCKED_ACTIONS,
"not_approval": True,
"request_sent": False,
"recipient_confirmed": False,
"owner_response_received": False,
"owner_response_accepted": False,
"owner_response_rejected": False,
"owner_response_quarantined": False,
"supplement_requested": False,
"live_evidence_received": False,
"live_config_hash_accepted": False,
"maintenance_window_accepted": False,
"restart_window_accepted": False,
"rollback_owner_accepted": False,
"post_check_plan_accepted": False,
"disable_switch_accepted": False,
"config_source_of_truth_accepted": False,
"service_dependency_map_accepted": False,
"port_binding_inventory_accepted": False,
"cold_start_sequence_accepted": False,
"incident_recovery_evidence_accepted": False,
"daemon_runner_contention_accepted": False,
"host_write_authorized": False,
"ssh_read_authorized": False,
"ssh_write_authorized": False,
"docker_compose_action_authorized": False,
"systemctl_action_authorized": False,
"repair_bot_execution_authorized": False,
"ansible_apply_authorized": False,
"sudo_action_authorized": False,
"secret_value_collection_allowed": False,
"active_scan_authorized": False,
"live_host_read_authorized": False,
"runtime_gate": False,
"action_buttons_allowed": False,
}
def build_report(
root: Path,
inventory: dict[str, Any],
request_draft_report: dict[str, Any],
generated_at: str | None,
) -> dict[str, Any]:
report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
requests = request_draft_report.get("request_drafts", [])
acceptance_candidates = [acceptance_candidate(item) for item in requests]
write_capable = [item for item in acceptance_candidates if item["write_capable_surface"]]
live_evidence = [item for item in acceptance_candidates if item["requires_live_evidence"]]
return {
"schema_version": "host_service_owner_response_acceptance_v1",
"generated_at": report_time,
"git_commit": git_short_sha(root),
"source_inventory_schema_version": inventory.get("schema_version"),
"source_inventory_status": inventory.get("status"),
"source_owner_request_schema_version": request_draft_report.get("schema_version"),
"source_owner_request_status": request_draft_report.get("status"),
"status": "owner_response_acceptance_ledger_ready_no_runtime_action",
"summary": {
"source_owner_request_count": len(requests),
"acceptance_candidate_count": len(acceptance_candidates),
"write_capable_acceptance_candidate_count": len(write_capable),
"live_evidence_required_candidate_count": len(live_evidence),
"acceptance_field_count": len(ACCEPTANCE_FIELDS),
"required_owner_field_count": len(acceptance_candidates[0]["required_owner_fields"]) if acceptance_candidates else 0,
"reviewer_check_count": len(REVIEWER_CHECKS),
"outcome_lane_count": len(OUTCOME_LANES),
"blocked_action_count": len(BLOCKED_ACTIONS),
"request_sent_count": 0,
"recipient_confirmed_count": 0,
"owner_response_received_count": 0,
"owner_response_accepted_count": 0,
"owner_response_rejected_count": 0,
"owner_response_quarantined_count": 0,
"supplement_requested_count": 0,
"live_evidence_received_count": 0,
"live_config_hash_accepted_count": 0,
"maintenance_window_accepted_count": 0,
"restart_window_accepted_count": 0,
"rollback_owner_accepted_count": 0,
"post_check_plan_accepted_count": 0,
"disable_switch_accepted_count": 0,
"config_source_of_truth_accepted_count": 0,
"service_dependency_map_accepted_count": 0,
"port_binding_inventory_accepted_count": 0,
"cold_start_sequence_accepted_count": 0,
"incident_recovery_evidence_accepted_count": 0,
"daemon_runner_contention_accepted_count": 0,
"host_write_authorized_count": 0,
"ssh_read_authorized_count": 0,
"ssh_write_authorized_count": 0,
"docker_compose_action_authorized_count": 0,
"systemctl_action_authorized_count": 0,
"repair_bot_execution_authorized_count": 0,
"ansible_apply_authorized_count": 0,
"sudo_action_authorized_count": 0,
"secret_value_collection_allowed_count": 0,
"active_scan_authorized_count": 0,
"live_host_read_authorized_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
},
"execution_boundaries": {
"request_dispatch_authorized": False,
"owner_response_accepted": False,
"live_host_read_authorized": False,
"host_write_authorized": False,
"ssh_read_authorized": False,
"ssh_write_authorized": False,
"docker_compose_action_authorized": False,
"systemctl_action_authorized": False,
"repair_bot_execution_authorized": False,
"ansible_apply_authorized": False,
"sudo_action_authorized": False,
"secret_value_collection_allowed": False,
"active_scan_authorized": False,
"runtime_execution_authorized": False,
"action_buttons_allowed": False,
"not_authorization": True,
},
"acceptance_fields": ACCEPTANCE_FIELDS,
"reviewer_checks": REVIEWER_CHECKS,
"outcome_lanes": OUTCOME_LANES,
"blocked_actions": BLOCKED_ACTIONS,
"acceptance_candidates": acceptance_candidates,
"next_steps": [
"等待 owner 以脫敏 metadata ref 回覆 live config hash、maintenance / restart window、rollback owner、post-check plan、disable switch、source of truth、依賴圖、port binding、cold-start sequence、incident recovery evidence 與 daemon / runner contention review。",
"收到回覆後先做欄位完整性、敏感 payload 隔離與 execution request 拒收,不得直接 host read、restart、repair-bot 或 Ansible apply。",
"write-capable 或事故回補 surface 必須額外 reviewer review且 runtime gate 需獨立人工批准、rollback、dependency / port / cold-start 與 post-check 成立。",
],
}
def main() -> int:
parser = argparse.ArgumentParser(description="IwoooS host service owner response acceptance 產生器")
parser.add_argument("--root", default=".", help="repo root")
parser.add_argument(
"--inventory-report",
default="docs/security/host-service-config-inventory.snapshot.json",
help="host-service-config-inventory.py 輸出的 JSON",
)
parser.add_argument(
"--owner-request-report",
default="docs/security/host-service-owner-request-draft.snapshot.json",
help="host-service-owner-request-draft.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()
inventory = load_json(root / args.inventory_report)
request_draft_report = load_json(root / args.owner_request_report)
report = build_report(root, inventory, request_draft_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(
"HOST_SERVICE_OWNER_RESPONSE_ACCEPTANCE_OK "
f"candidates={summary['acceptance_candidate_count']} "
f"write_capable={summary['write_capable_acceptance_candidate_count']} "
f"checks={summary['reviewer_check_count']} "
f"lanes={summary['outcome_lane_count']} "
f"accepted={summary['owner_response_accepted_count']} "
f"runtime_gate={summary['runtime_gate_count']}",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())