543 lines
27 KiB
Python
543 lines
27 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
IwoooS Backup / Restore / Escrow post-incident readback 只讀計畫產生器。
|
||
|
||
本工具讀取 Backup / Restore / Escrow owner response acceptance snapshot,
|
||
建立事故後回讀計畫:誰觸發或觀察到 backup / restore / offsite / escrow
|
||
異常、改前改後 freshness / restore / offsite / retention 狀態、是否有
|
||
no-false-green 與 rollback / stop condition。它不執行 backup、不 restore、
|
||
不 rclone / restic / Velero、不寫 escrow marker、不 SSH、不 kubectl、不讀
|
||
secret value、不保存 raw backup listing、不寫 production。
|
||
"""
|
||
|
||
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))
|
||
|
||
READBACK_FIELDS = [
|
||
"post_incident_readback_candidate_id",
|
||
"source_acceptance_candidate_id",
|
||
"request_id",
|
||
"surface_id",
|
||
"config_kind",
|
||
"backup_scope",
|
||
"control_tier",
|
||
"write_capable_surface",
|
||
"requires_live_evidence",
|
||
"backup_incident_or_change_ref",
|
||
"actor_attribution_ref",
|
||
"change_or_outage_time_window_ref",
|
||
"change_intent_or_break_glass_ref",
|
||
"before_backup_freshness_state_ref",
|
||
"after_backup_freshness_state_ref",
|
||
"backup_status_readback_ref",
|
||
"restore_drill_readback_ref",
|
||
"restore_target_isolation_readback_ref",
|
||
"offsite_sync_readback_ref",
|
||
"offsite_remote_delete_guard_readback_ref",
|
||
"credential_escrow_non_secret_readback_ref",
|
||
"credential_recovery_drill_readback_ref",
|
||
"retention_runway_readback_ref",
|
||
"retention_or_prune_decision_ref",
|
||
"backup_dependency_map_readback_ref",
|
||
"data_classification_readback_ref",
|
||
"restore_observer_stop_condition_ref",
|
||
"backup_health_no_false_green_readback_ref",
|
||
"alert_textfile_readback_ref",
|
||
"cold_start_dr_scorecard_ref",
|
||
"cross_project_sync_ref",
|
||
"rollback_validation_ref",
|
||
"post_change_monitoring_ref",
|
||
"postcheck_readback_ref",
|
||
"recurrence_guard_ref",
|
||
"maintenance_window",
|
||
"rollback_owner",
|
||
"followup_owner",
|
||
"redacted_evidence_refs",
|
||
"reviewer_outcome",
|
||
"no_secret_value_attestation",
|
||
"no_raw_backup_payload_attestation",
|
||
"no_production_restore_attestation",
|
||
"no_false_green_attestation",
|
||
"not_approval",
|
||
]
|
||
|
||
REQUIRED_READBACK_FIELDS = [
|
||
"backup_incident_or_change_ref",
|
||
"actor_attribution_ref",
|
||
"change_or_outage_time_window_ref",
|
||
"change_intent_or_break_glass_ref",
|
||
"before_backup_freshness_state_ref",
|
||
"after_backup_freshness_state_ref",
|
||
"backup_status_readback_ref",
|
||
"restore_drill_readback_ref",
|
||
"restore_target_isolation_readback_ref",
|
||
"offsite_sync_readback_ref",
|
||
"offsite_remote_delete_guard_readback_ref",
|
||
"credential_escrow_non_secret_readback_ref",
|
||
"credential_recovery_drill_readback_ref",
|
||
"retention_runway_readback_ref",
|
||
"retention_or_prune_decision_ref",
|
||
"backup_dependency_map_readback_ref",
|
||
"data_classification_readback_ref",
|
||
"restore_observer_stop_condition_ref",
|
||
"backup_health_no_false_green_readback_ref",
|
||
"alert_textfile_readback_ref",
|
||
"cold_start_dr_scorecard_ref",
|
||
"cross_project_sync_ref",
|
||
"rollback_validation_ref",
|
||
"post_change_monitoring_ref",
|
||
"postcheck_readback_ref",
|
||
"recurrence_guard_ref",
|
||
"maintenance_window",
|
||
"rollback_owner",
|
||
"followup_owner",
|
||
"redacted_evidence_refs",
|
||
"no_secret_value_attestation",
|
||
"no_raw_backup_payload_attestation",
|
||
"no_production_restore_attestation",
|
||
"no_false_green_attestation",
|
||
]
|
||
|
||
REVIEWER_CHECKS = [
|
||
{"check_id": "source_owner_response_acceptance_current", "instruction": "來源 owner response acceptance snapshot 必須是目前版本。"},
|
||
{"check_id": "incident_or_change_ref_present", "instruction": "必須有 incident、change、outage、ticket 或 maintenance ref,不能只寫備份正常。"},
|
||
{"check_id": "actor_attribution_present", "instruction": "必須標示 actor role / team,不接受匿名 backup、restore、sync、prune 或 escrow marker write。"},
|
||
{"check_id": "change_or_outage_time_window_present", "instruction": "必須有變更 / 異常時間窗,供 freshness、offsite、alert 與 cold-start scorecard 對齊。"},
|
||
{"check_id": "intent_or_break_glass_present", "instruction": "正常變更需有 change intent;緊急變更需有 break-glass reason,但 break-glass 不等於事前批准。"},
|
||
{"check_id": "before_after_freshness_present", "instruction": "必須有 before / after backup freshness state ref,不得只看最新一次成功。"},
|
||
{"check_id": "backup_status_readback_present", "instruction": "backup status 只能收 owner-provided redacted ref,不得讀 raw backup store 或未脫敏 listing。"},
|
||
{"check_id": "restore_drill_readback_present", "instruction": "restore drill 必須是 readback / approval metadata,不得是 production restore 執行要求。"},
|
||
{"check_id": "restore_target_isolation_present", "instruction": "必須有隔離 restore target 或明確 no-production-write 邊界。"},
|
||
{"check_id": "offsite_sync_readback_present", "instruction": "offsite sync 必須有 redacted state ref,不得保存 raw remote path、object listing 或 rclone config。"},
|
||
{"check_id": "remote_delete_guard_present", "instruction": "任何 latest-only、remote delete 或 retention policy 必須有 remote delete guard 與 owner ref。"},
|
||
{"check_id": "credential_escrow_non_secret_present", "instruction": "credential escrow 只能是 non-secret proof / marker ref,不得包含 value、hash、seed、recovery code 或 partial token。"},
|
||
{"check_id": "credential_recovery_drill_metadata_only", "instruction": "credential recovery drill 必須以 metadata / evidence id 呈現,不保存 secret derivative。"},
|
||
{"check_id": "retention_runway_present", "instruction": "retention / prune 必須有可恢復窗口、runway、撤回條件與 owner。"},
|
||
{"check_id": "retention_or_prune_decision_present", "instruction": "retention 或 prune decision 必須獨立列出,不得與 offsite sync 成功混在一起。"},
|
||
{"check_id": "dependency_map_present", "instruction": "必須列出 DB、repo、registry、object storage、K8s、monitoring、credential escrow 與 DR runbook 依賴圖 ref。"},
|
||
{"check_id": "data_classification_present", "instruction": "必須標示備份集資料分級;不得要求 raw customer data、payload 或 unredacted archive listing。"},
|
||
{"check_id": "restore_observer_stop_condition_present", "instruction": "restore / drill 必須有 observer、stop condition、rollback owner 與停損條件。"},
|
||
{"check_id": "backup_health_no_false_green_present", "instruction": "backup health / textfile / alert evidence 必須防止 false green,不能只看單一 healthy、route 200 或 dashboard up。"},
|
||
{"check_id": "alert_textfile_readback_present", "instruction": "若涉及告警或 textfile exporter,必須回讀 stale / partial / missing marker,不得只看 alert quiet。"},
|
||
{"check_id": "cold_start_scorecard_present", "instruction": "若影響冷啟動或 DR,必須有 cold-start / scorecard ref,且不能偽造 credential escrow evidence。"},
|
||
{"check_id": "cross_project_sync_present", "instruction": "若影響 AwoooP、IwoooS、momo、Gitea、Harbor、monitoring 或公開站,需有跨專案同步 ref。"},
|
||
{"check_id": "rollback_validation_present", "instruction": "必須提供 rollback validation ref,包含 rollback owner 與回復方式。"},
|
||
{"check_id": "post_change_monitoring_present", "instruction": "必須有 post-change monitoring window,觀察 freshness、offsite、restore target、alerts 與 scorecard。"},
|
||
{"check_id": "postcheck_independent", "instruction": "post-check 必須獨立於原操作人、單一 backup status 與 UI 顯示。"},
|
||
{"check_id": "recurrence_guard_present", "instruction": "必須提出防再發 guard、owner review、retention freeze、remote-delete block 或 automation block。"},
|
||
{"check_id": "maintenance_window_present", "instruction": "後續任何 backup、restore、offsite sync、prune、escrow marker 或 Velero 動作都需維護窗口。"},
|
||
{"check_id": "redacted_refs_only", "instruction": "evidence 只能是脫敏 ref、hash、ticket、commit 或 artifact pointer。"},
|
||
{"check_id": "secret_value_absent", "instruction": "不得出現 token、private key、rclone config、restic password、seed、kubeconfig、recovery code、credential URL 或 partial secret。"},
|
||
{"check_id": "raw_payload_absent", "instruction": "不得保存 raw backup listing、raw restore payload、raw archive content、raw object path、raw DB dump 或未脫敏 screenshot。"},
|
||
{"check_id": "no_false_green", "instruction": "不得只用 backup success、route 200、dashboard up、alert quiet、textfile present、UI 可見或 CD success 當 DR / backup 驗收。"},
|
||
{"check_id": "runtime_stays_zero", "instruction": "readback plan 不得觸發 backup、restore、offsite sync、remote delete、retention change、prune、Velero、kubectl、SSH 或 production write。"},
|
||
]
|
||
|
||
OUTCOME_LANES = [
|
||
{"lane_id": "waiting_post_incident_readback", "meaning": "尚未收到 Backup / Restore / Escrow 事故回讀包;所有 accepted / runtime count 維持 0。"},
|
||
{"lane_id": "request_actor_or_time_supplement", "meaning": "缺 actor、change / outage time window、intent 或 break-glass reason 時要求補件。"},
|
||
{"lane_id": "request_backup_freshness_supplement", "meaning": "缺 before / after freshness、backup status、alert textfile 或 scorecard 時要求補件。"},
|
||
{"lane_id": "request_restore_isolation_supplement", "meaning": "缺 restore drill、隔離目標、observer、stop condition 或 rollback validation 時要求補件。"},
|
||
{"lane_id": "request_offsite_retention_supplement", "meaning": "缺 offsite sync、remote delete guard、retention runway 或 prune decision 時要求補件。"},
|
||
{"lane_id": "request_escrow_non_secret_supplement", "meaning": "缺 credential escrow non-secret proof 或 recovery drill metadata 時要求補件。"},
|
||
{"lane_id": "quarantine_raw_payload", "meaning": "收到 secret、raw backup listing、raw restore payload、raw DB dump、rclone config 或未脫敏截圖時只能隔離。"},
|
||
{"lane_id": "reject_false_green_claim", "meaning": "把 backup success、route 200、dashboard up、alert quiet、textfile present 或 UI 可見當驗收時拒收。"},
|
||
{"lane_id": "ready_for_backup_restore_post_incident_review", "meaning": "metadata 合格後,只能進 backup / restore reviewer review。"},
|
||
{"lane_id": "recurrence_guard_backfill_required", "meaning": "需補防再發 guard、retention freeze、remote-delete block、owner review 或 automation block。"},
|
||
{"lane_id": "waiting_runtime_gate", "meaning": "即使 readback accepted,runtime gate 仍需獨立人工批准。"},
|
||
]
|
||
|
||
BLOCKED_ACTIONS = [
|
||
"backup_run",
|
||
"restore_run",
|
||
"restore_drill",
|
||
"production_restore",
|
||
"offsite_sync",
|
||
"offsite_remote_delete",
|
||
"credential_escrow_marker_write",
|
||
"credential_recovery_execution",
|
||
"retention_change",
|
||
"retention_prune",
|
||
"restic_prune",
|
||
"rclone_config_read",
|
||
"rclone_config_change",
|
||
"velero_restore",
|
||
"velero_backup",
|
||
"kubectl_action",
|
||
"ssh_read",
|
||
"ssh_write",
|
||
"secret_value_collection",
|
||
"secret_hash_collection",
|
||
"partial_token_collection",
|
||
"restic_password_collection",
|
||
"rclone_token_collection",
|
||
"kubeconfig_collection",
|
||
"host_write",
|
||
"active_scan",
|
||
"production_write",
|
||
"runtime_gate_open",
|
||
"raw_backup_payload_storage",
|
||
"raw_restore_payload_storage",
|
||
"raw_object_listing_storage",
|
||
"raw_db_dump_storage",
|
||
"accept_secret_value_evidence",
|
||
"accept_credential_derivative_evidence",
|
||
"mark_readback_accepted_without_reviewer_record",
|
||
"accept_backup_success_without_freshness_slo",
|
||
"accept_restore_without_isolated_target",
|
||
"accept_offsite_without_remote_delete_guard",
|
||
"accept_retention_without_runway",
|
||
"accept_credential_recovery_without_non_secret_proof",
|
||
"accept_backup_health_false_green",
|
||
"accept_alert_quiet_as_backup_healthy",
|
||
"accept_textfile_present_as_dr_complete",
|
||
"skip_dependency_map_review",
|
||
"skip_data_classification_review",
|
||
"skip_restore_observer_stop_condition",
|
||
"skip_cross_project_sync",
|
||
"skip_rollback_validation",
|
||
"skip_post_change_monitoring",
|
||
"forge_credential_escrow_evidence",
|
||
"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 readback_candidate(item: dict[str, Any]) -> dict[str, Any]:
|
||
surface_id = item["surface_id"]
|
||
candidate_id = f"backup_restore_post_incident_readback:{surface_id}"
|
||
return {
|
||
"post_incident_readback_candidate_id": candidate_id,
|
||
"status": "waiting_post_incident_readback",
|
||
"source_acceptance_candidate_id": item["acceptance_candidate_id"],
|
||
"request_id": item["request_id"],
|
||
"surface_id": surface_id,
|
||
"config_kind": item["config_kind"],
|
||
"backup_scope": item["backup_scope"],
|
||
"control_tier": item["control_tier"],
|
||
"write_capable_surface": item["write_capable_surface"],
|
||
"requires_live_evidence": True,
|
||
"backup_incident_or_change_ref": None,
|
||
"actor_attribution_ref": None,
|
||
"change_or_outage_time_window_ref": None,
|
||
"change_intent_or_break_glass_ref": None,
|
||
"before_backup_freshness_state_ref": None,
|
||
"after_backup_freshness_state_ref": None,
|
||
"backup_status_readback_ref": None,
|
||
"restore_drill_readback_ref": None,
|
||
"restore_target_isolation_readback_ref": None,
|
||
"offsite_sync_readback_ref": None,
|
||
"offsite_remote_delete_guard_readback_ref": None,
|
||
"credential_escrow_non_secret_readback_ref": None,
|
||
"credential_recovery_drill_readback_ref": None,
|
||
"retention_runway_readback_ref": None,
|
||
"retention_or_prune_decision_ref": None,
|
||
"backup_dependency_map_readback_ref": None,
|
||
"data_classification_readback_ref": None,
|
||
"restore_observer_stop_condition_ref": None,
|
||
"backup_health_no_false_green_readback_ref": None,
|
||
"alert_textfile_readback_ref": None,
|
||
"cold_start_dr_scorecard_ref": None,
|
||
"cross_project_sync_ref": None,
|
||
"rollback_validation_ref": None,
|
||
"post_change_monitoring_ref": None,
|
||
"postcheck_readback_ref": None,
|
||
"recurrence_guard_ref": None,
|
||
"maintenance_window": "pending_post_incident_readback",
|
||
"rollback_owner": "pending_post_incident_readback",
|
||
"followup_owner": "pending_post_incident_readback",
|
||
"redacted_evidence_refs": [],
|
||
"reviewer_outcome": "waiting_post_incident_readback",
|
||
"readback_fields": READBACK_FIELDS,
|
||
"required_readback_fields": REQUIRED_READBACK_FIELDS,
|
||
"reviewer_checks": [entry["check_id"] for entry in REVIEWER_CHECKS],
|
||
"outcome_lanes": [entry["lane_id"] for entry in OUTCOME_LANES],
|
||
"blocked_actions": BLOCKED_ACTIONS,
|
||
"not_approval": True,
|
||
"post_incident_readback_received": False,
|
||
"post_incident_readback_accepted": False,
|
||
"actor_attribution_accepted": False,
|
||
"before_after_freshness_accepted": False,
|
||
"backup_status_readback_accepted": False,
|
||
"restore_drill_readback_accepted": False,
|
||
"restore_target_isolation_readback_accepted": False,
|
||
"offsite_sync_readback_accepted": False,
|
||
"offsite_remote_delete_guard_accepted": False,
|
||
"credential_escrow_non_secret_readback_accepted": False,
|
||
"credential_recovery_drill_readback_accepted": False,
|
||
"retention_runway_readback_accepted": False,
|
||
"retention_or_prune_decision_accepted": False,
|
||
"backup_dependency_map_readback_accepted": False,
|
||
"data_classification_readback_accepted": False,
|
||
"restore_observer_stop_condition_accepted": False,
|
||
"backup_health_no_false_green_readback_accepted": False,
|
||
"alert_textfile_readback_accepted": False,
|
||
"cold_start_dr_scorecard_accepted": False,
|
||
"cross_project_sync_accepted": False,
|
||
"rollback_validation_accepted": False,
|
||
"post_change_monitoring_accepted": False,
|
||
"postcheck_readback_accepted": False,
|
||
"recurrence_guard_accepted": False,
|
||
"no_false_green_accepted": False,
|
||
"backup_run_authorized": False,
|
||
"restore_run_authorized": False,
|
||
"offsite_sync_authorized": False,
|
||
"offsite_remote_delete_authorized": False,
|
||
"credential_escrow_marker_write_authorized": False,
|
||
"credential_recovery_execution_authorized": False,
|
||
"retention_change_authorized": False,
|
||
"restic_prune_authorized": False,
|
||
"rclone_config_authorized": False,
|
||
"velero_restore_authorized": False,
|
||
"velero_backup_authorized": False,
|
||
"kubectl_action_authorized": False,
|
||
"ssh_read_authorized": False,
|
||
"ssh_write_authorized": False,
|
||
"secret_value_collection_allowed": False,
|
||
"host_write_authorized": False,
|
||
"active_scan_authorized": False,
|
||
"production_write_authorized": False,
|
||
"runtime_gate": False,
|
||
"action_buttons_allowed": False,
|
||
}
|
||
|
||
|
||
def searchable_candidate_text(item: dict[str, Any]) -> str:
|
||
scopes = item.get("backup_scope", [])
|
||
return " ".join(
|
||
[
|
||
str(item.get("surface_id", "")),
|
||
str(item.get("config_kind", "")),
|
||
" ".join(str(scope) for scope in scopes),
|
||
]
|
||
).lower()
|
||
|
||
|
||
def candidate_has_any(item: dict[str, Any], keywords: set[str]) -> bool:
|
||
text = searchable_candidate_text(item)
|
||
return any(keyword in text for keyword in keywords)
|
||
|
||
|
||
def build_report(root: Path, source: dict[str, Any], generated_at: str | None) -> dict[str, Any]:
|
||
report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
|
||
source_candidates = source.get("acceptance_candidates", [])
|
||
candidates = [readback_candidate(item) for item in source_candidates]
|
||
|
||
def count_true(key: str) -> int:
|
||
return sum(1 for item in candidates if item[key])
|
||
|
||
summary = {
|
||
"source_acceptance_candidate_count": source["summary"]["acceptance_candidate_count"],
|
||
"source_write_capable_acceptance_candidate_count": source["summary"]["write_capable_acceptance_candidate_count"],
|
||
"source_live_evidence_required_candidate_count": source["summary"]["live_evidence_required_candidate_count"],
|
||
"source_required_owner_field_count": source["summary"]["required_owner_field_count"],
|
||
"source_reviewer_check_count": source["summary"]["reviewer_check_count"],
|
||
"source_blocked_action_count": source["summary"]["blocked_action_count"],
|
||
"source_owner_response_accepted_count": source["summary"]["owner_response_accepted_count"],
|
||
"source_runtime_gate_count": source["summary"]["runtime_gate_count"],
|
||
"readback_candidate_count": len(candidates),
|
||
"write_capable_readback_candidate_count": count_true("write_capable_surface"),
|
||
"live_evidence_required_readback_candidate_count": count_true("requires_live_evidence"),
|
||
"restore_drill_readback_required_candidate_count": len(candidates),
|
||
"offsite_or_escrow_readback_required_candidate_count": sum(
|
||
1
|
||
for item in candidates
|
||
if candidate_has_any(
|
||
item,
|
||
{
|
||
"offsite",
|
||
"escrow",
|
||
"credential",
|
||
"rclone",
|
||
"b2",
|
||
"velero",
|
||
"object storage",
|
||
},
|
||
)
|
||
),
|
||
"retention_or_remote_delete_readback_required_candidate_count": sum(
|
||
1
|
||
for item in candidates
|
||
if candidate_has_any(
|
||
item,
|
||
{
|
||
"retention",
|
||
"prune",
|
||
"remote_delete",
|
||
"latest-only",
|
||
"restic",
|
||
"offsite",
|
||
},
|
||
)
|
||
),
|
||
"cross_project_sync_required_candidate_count": len(candidates),
|
||
"no_false_green_required_candidate_count": len(candidates),
|
||
"readback_field_count": len(READBACK_FIELDS),
|
||
"required_readback_field_count": len(REQUIRED_READBACK_FIELDS),
|
||
"reviewer_check_count": len(REVIEWER_CHECKS),
|
||
"outcome_lane_count": len(OUTCOME_LANES),
|
||
"blocked_action_count": len(BLOCKED_ACTIONS),
|
||
"post_incident_readback_received_count": 0,
|
||
"post_incident_readback_accepted_count": 0,
|
||
"actor_attribution_accepted_count": 0,
|
||
"before_after_freshness_accepted_count": 0,
|
||
"backup_status_readback_accepted_count": 0,
|
||
"restore_drill_readback_accepted_count": 0,
|
||
"restore_target_isolation_readback_accepted_count": 0,
|
||
"offsite_sync_readback_accepted_count": 0,
|
||
"offsite_remote_delete_guard_accepted_count": 0,
|
||
"credential_escrow_non_secret_readback_accepted_count": 0,
|
||
"credential_recovery_drill_readback_accepted_count": 0,
|
||
"retention_runway_readback_accepted_count": 0,
|
||
"retention_or_prune_decision_accepted_count": 0,
|
||
"backup_dependency_map_readback_accepted_count": 0,
|
||
"data_classification_readback_accepted_count": 0,
|
||
"restore_observer_stop_condition_accepted_count": 0,
|
||
"backup_health_no_false_green_readback_accepted_count": 0,
|
||
"alert_textfile_readback_accepted_count": 0,
|
||
"cold_start_dr_scorecard_accepted_count": 0,
|
||
"cross_project_sync_accepted_count": 0,
|
||
"rollback_validation_accepted_count": 0,
|
||
"post_change_monitoring_accepted_count": 0,
|
||
"postcheck_readback_accepted_count": 0,
|
||
"recurrence_guard_accepted_count": 0,
|
||
"no_false_green_accepted_count": 0,
|
||
"backup_run_authorized_count": 0,
|
||
"restore_run_authorized_count": 0,
|
||
"offsite_sync_authorized_count": 0,
|
||
"offsite_remote_delete_authorized_count": 0,
|
||
"credential_escrow_marker_write_authorized_count": 0,
|
||
"credential_recovery_execution_authorized_count": 0,
|
||
"retention_change_authorized_count": 0,
|
||
"restic_prune_authorized_count": 0,
|
||
"rclone_config_authorized_count": 0,
|
||
"velero_restore_authorized_count": 0,
|
||
"velero_backup_authorized_count": 0,
|
||
"kubectl_action_authorized_count": 0,
|
||
"ssh_read_authorized_count": 0,
|
||
"ssh_write_authorized_count": 0,
|
||
"secret_value_collection_allowed_count": 0,
|
||
"host_write_authorized_count": 0,
|
||
"active_scan_authorized_count": 0,
|
||
"production_write_authorized_count": 0,
|
||
"runtime_gate_count": 0,
|
||
"action_button_count": 0,
|
||
"coverage_percent_after_readback_plan": 66,
|
||
}
|
||
|
||
return {
|
||
"schema_version": "backup_restore_post_incident_readback_plan_v1",
|
||
"generated_at": report_time,
|
||
"git_commit": git_short_sha(root),
|
||
"mode": "metadata_only_no_backup_no_restore_no_secret_value",
|
||
"source_schema_version": source["schema_version"],
|
||
"source_status": source["status"],
|
||
"source_paths": [
|
||
"docs/security/BACKUP-RESTORE-ESCROW-INVENTORY.md",
|
||
"docs/security/backup-restore-escrow-inventory.snapshot.json",
|
||
"docs/security/BACKUP-RESTORE-OWNER-RESPONSE-ACCEPTANCE.md",
|
||
"docs/security/backup-restore-owner-response-acceptance.snapshot.json",
|
||
],
|
||
"status": "post_incident_readback_plan_ready_no_runtime_action",
|
||
"readback_candidates": candidates,
|
||
"readback_fields": READBACK_FIELDS,
|
||
"required_readback_fields": REQUIRED_READBACK_FIELDS,
|
||
"reviewer_checks": REVIEWER_CHECKS,
|
||
"outcome_lanes": OUTCOME_LANES,
|
||
"blocked_actions": BLOCKED_ACTIONS,
|
||
"execution_boundaries": {
|
||
"not_authorization": True,
|
||
"backup_run_authorized": False,
|
||
"restore_run_authorized": False,
|
||
"offsite_sync_authorized": False,
|
||
"offsite_remote_delete_authorized": False,
|
||
"credential_escrow_marker_write_authorized": False,
|
||
"credential_recovery_execution_authorized": False,
|
||
"retention_change_authorized": False,
|
||
"restic_prune_authorized": False,
|
||
"rclone_config_authorized": False,
|
||
"velero_restore_authorized": False,
|
||
"velero_backup_authorized": False,
|
||
"kubectl_action_authorized": False,
|
||
"ssh_read_authorized": False,
|
||
"ssh_write_authorized": False,
|
||
"secret_value_collection_allowed": False,
|
||
"host_write_authorized": False,
|
||
"active_scan_authorized": False,
|
||
"production_write_authorized": False,
|
||
"runtime_gate": False,
|
||
"action_buttons_allowed": False,
|
||
"raw_backup_payload_storage_allowed": False,
|
||
},
|
||
"operator_interpretation": [
|
||
"此計畫只定義 Backup / Restore / Escrow 事故後回讀欄位,不代表 live backup store 已讀取或可讀取。",
|
||
"backup success、route 200、dashboard up、alert quiet、textfile present、UI 可見或 CD success 都不能單獨當成 DR / backup 健康。",
|
||
"未來若要 backup、restore、offsite sync、remote delete、retention / prune、escrow marker、Velero、kubectl、SSH 或 production write,必須另有維護窗口、rollback owner 與人工批准。",
|
||
],
|
||
"summary": summary,
|
||
}
|
||
|
||
|
||
def write_report(report: dict[str, Any], output: Path | None) -> None:
|
||
text = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n"
|
||
if output:
|
||
output.write_text(text, encoding="utf-8")
|
||
else:
|
||
sys.stdout.write(text)
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser()
|
||
parser.add_argument("--root", default=".", help="Repository root")
|
||
parser.add_argument("--generated-at", default=None, help="Override generated_at")
|
||
parser.add_argument("--output", default=None, help="Output snapshot path")
|
||
args = parser.parse_args()
|
||
|
||
root = Path(args.root).resolve()
|
||
source = load_json(root / "docs/security/backup-restore-owner-response-acceptance.snapshot.json")
|
||
report = build_report(root, source, args.generated_at)
|
||
output = Path(args.output).resolve() if args.output else None
|
||
write_report(report, output)
|
||
summary = report["summary"]
|
||
print(
|
||
"BACKUP_RESTORE_POST_INCIDENT_READBACK_PLAN_OK "
|
||
f"candidates={summary['readback_candidate_count']} "
|
||
f"write_capable={summary['write_capable_readback_candidate_count']} "
|
||
f"checks={summary['reviewer_check_count']} "
|
||
f"lanes={summary['outcome_lane_count']} "
|
||
f"accepted={summary['post_incident_readback_accepted_count']} "
|
||
f"runtime_gate={summary['runtime_gate_count']}"
|
||
)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|