Files
awoooi/scripts/security/cd-runner-secret-injection-post-incident-readback-plan.py
Your Name bb459d59f9
Some checks failed
Code Review / ai-code-review (push) Successful in 15s
CD Pipeline / tests (push) Successful in 1m43s
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
feat(iwooos): 新增 CD runner secret 事故回讀 gate
2026-06-16 11:42:38 +08:00

490 lines
25 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 CD / Runner / Secret injection post-incident readback 只讀計畫產生器。
本工具讀取 CD / Runner / Secret injection change evidence acceptance snapshot
建立事故後回讀計畫:誰改了 workflow / runner / secret injection、何時異常、
runner / secret name / deploy marker / notification 是否可回讀、rollback 與防再發。
它不呼叫 Gitea / GitHub API、不讀 secret store、不讀 secret value、不修改 workflow、
不啟用 runner、不 rotate secret、不 dispatch workflow、不觸發部署、不寫 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_change_evidence_candidate_id",
"title",
"control_tier",
"risk",
"source_refs",
"affected_scope",
"workflow_secret_name_refs",
"write_capable",
"incident_or_change_ref",
"actor_attribution_ref",
"change_time_window_ref",
"change_intent_or_break_glass_ref",
"workflow_diff_state_ref",
"runner_attestation_state_ref",
"runner_executor_host_readback_ref",
"runner_workspace_cleanup_readback_ref",
"runner_permission_scope_ref",
"secret_name_parity_state_ref",
"secret_injection_route_state_ref",
"step_env_secret_guard_result_ref",
"log_redaction_readback_ref",
"deploy_marker_readback_ref",
"gitea_action_run_readback_ref",
"webhook_delivery_state_ref",
"deploy_key_branch_protection_codeowners_ref",
"notification_delivery_receipt_ref",
"before_after_deploy_state_ref",
"affected_route_or_service_state_ref",
"cross_project_sync_ref",
"rollback_validation_ref",
"postcheck_evidence_ref",
"post_change_monitoring_ref",
"recurrence_guard_ref",
"maintenance_window",
"rollback_owner",
"followup_owner",
"redacted_evidence_refs",
"reviewer_outcome",
"no_secret_value_attestation",
"no_raw_workflow_payload_attestation",
"no_unredacted_log_attestation",
"no_false_green_attestation",
"not_approval",
]
REQUIRED_READBACK_FIELDS = [
"incident_or_change_ref",
"actor_attribution_ref",
"change_time_window_ref",
"change_intent_or_break_glass_ref",
"workflow_diff_state_ref",
"runner_attestation_state_ref",
"runner_executor_host_readback_ref",
"runner_workspace_cleanup_readback_ref",
"runner_permission_scope_ref",
"secret_name_parity_state_ref",
"secret_injection_route_state_ref",
"step_env_secret_guard_result_ref",
"log_redaction_readback_ref",
"deploy_marker_readback_ref",
"gitea_action_run_readback_ref",
"webhook_delivery_state_ref",
"deploy_key_branch_protection_codeowners_ref",
"notification_delivery_receipt_ref",
"before_after_deploy_state_ref",
"affected_route_or_service_state_ref",
"cross_project_sync_ref",
"rollback_validation_ref",
"postcheck_evidence_ref",
"post_change_monitoring_ref",
"recurrence_guard_ref",
"maintenance_window",
"rollback_owner",
"followup_owner",
"redacted_evidence_refs",
"no_secret_value_attestation",
"no_raw_workflow_payload_attestation",
"no_unredacted_log_attestation",
"no_false_green_attestation",
]
REVIEWER_CHECKS = [
{"check_id": "source_change_evidence_acceptance_current", "instruction": "來源 change evidence acceptance snapshot 必須是目前版本。"},
{"check_id": "incident_or_change_ref_present", "instruction": "必須有 incident、change、outage、ticket 或 maintenance ref不能只寫 CD 成功。"},
{"check_id": "actor_attribution_present", "instruction": "必須標示 actor role / team不接受匿名 workflow dispatch、runner restart 或 secret injection change。"},
{"check_id": "change_time_window_present", "instruction": "必須有變更 / 異常時間窗,供 Gitea run、deploy marker 與通知回執對齊。"},
{"check_id": "intent_or_break_glass_present", "instruction": "正常變更需有 change intent緊急變更需有 break-glass reason但 break-glass 不等於事前批准。"},
{"check_id": "workflow_diff_state_present", "instruction": "必須回讀 workflow diff state不得保存 raw workflow payload 或未脫敏 patch。"},
{"check_id": "runner_attestation_present", "instruction": "runner label、executor、host alias、owner 與維護窗口必須可追溯。"},
{"check_id": "runner_executor_host_readback_present", "instruction": "必須回讀 runner executor / host / container boundary不得只看 job success。"},
{"check_id": "runner_workspace_cleanup_present", "instruction": "必須回讀 workspace cleanup、root-owned cache、artifact residue 或 shared Docker daemon 影響。"},
{"check_id": "runner_permission_scope_present", "instruction": "必須回讀 runner permission、token scope、hosted runner minutes / supply-chain 風險。"},
{"check_id": "secret_name_parity_present", "instruction": "secret parity 只能保存 secret name / scope / present-absent / owner metadata。"},
{"check_id": "secret_injection_route_present", "instruction": "涉及 CD / K8s secret injection 時必須標出 injection path 與 owner不得讀 value。"},
{"check_id": "step_env_secret_guard_present", "instruction": "必須附 `check-gitea-step-env-secrets` 或等價 guard result ref。"},
{"check_id": "log_redaction_readback_present", "instruction": "必須回讀 Actions log / notification log 未展開 secret value、hash 或 partial token。"},
{"check_id": "deploy_marker_readback_present", "instruction": "deploy marker 只能當部署證據,不代表資安 runtime approval。"},
{"check_id": "gitea_action_run_readback_present", "instruction": "Gitea Actions run readback 只能是 run id / job id / status ref不保存 token 或 cookie。"},
{"check_id": "webhook_delivery_state_present", "instruction": "涉及 webhook 時必須回讀 delivery state 與 owner不保存 webhook secret。"},
{"check_id": "deploy_key_branch_protection_codeowners_present", "instruction": "影響 deploy key、required checks、CODEOWNERS 或 branch protection 時必須標出影響。"},
{"check_id": "notification_delivery_receipt_present", "instruction": "涉及通知時必須有 SRE route owner 與 delivery receipt metadata ref。"},
{"check_id": "before_after_deploy_state_present", "instruction": "必須有 before / after deploy state ref不得只看最新 route 200。"},
{"check_id": "affected_route_or_service_state_present", "instruction": "需列 affected route、service、API、admin、webhook、AI provider 或監控影響。"},
{"check_id": "cross_project_sync_present", "instruction": "若影響 AwoooP、IwoooS、agent-bounty、監控或公開服務需有跨專案同步 ref。"},
{"check_id": "rollback_validation_present", "instruction": "必須提供 rollback validation ref包含 rollback owner 與回復方式。"},
{"check_id": "postcheck_independent", "instruction": "post-check 必須獨立於原操作人、workflow success 與 UI 顯示。"},
{"check_id": "post_change_monitoring_present", "instruction": "必須有 post-change monitoring window觀察 route、error、alert、deploy marker 與 notification receipt。"},
{"check_id": "recurrence_guard_present", "instruction": "必須提出防再發 guard、owner review、change freeze 或 automation block。"},
{"check_id": "redacted_refs_only", "instruction": "evidence 只能是脫敏 ref、commit、run id、job id、ticket、hash 或 artifact pointer。"},
{"check_id": "secret_material_absent", "instruction": "不得保存 secret value、hash、masked token、partial token、runner token、webhook secret、deploy key private material、cookie、authorization header 或完整 credential URL。"},
{"check_id": "no_false_green", "instruction": "不得只用 CD success、deploy marker、workflow success、route 200、runner online、UI 可見或 AwoooP approval 當驗收。"},
{"check_id": "runtime_stays_zero", "instruction": "readback plan 不得觸發 workflow dispatch、runner change、secret change、webhook change、deploy key change、branch protection change、refs sync、ArgoCD sync、K8s secret injection 或 production deploy。"},
]
OUTCOME_LANES = [
{"lane_id": "waiting_post_incident_readback", "meaning": "尚未收到 CD / runner / secret injection 事故回讀包;所有 accepted / runtime count 維持 0。"},
{"lane_id": "request_actor_or_time_supplement", "meaning": "缺 actor、change time window、intent 或 break-glass reason 時要求補件。"},
{"lane_id": "request_workflow_runner_supplement", "meaning": "缺 workflow diff、runner attestation、executor / host、workspace cleanup 或 permission scope 時要求補件。"},
{"lane_id": "request_secret_injection_supplement", "meaning": "缺 secret name parity、injection route、step-env guard 或 log redaction readback 時要求補件。"},
{"lane_id": "request_deploy_run_supplement", "meaning": "缺 deploy marker、Gitea run readback、before / after deploy state 或 post-check 時要求補件。"},
{"lane_id": "request_webhook_notification_supplement", "meaning": "缺 webhook delivery、notification receipt、SRE route owner 或 cross-project sync 時要求補件。"},
{"lane_id": "quarantine_sensitive_payload", "meaning": "收到 secret value、hash、runner token、webhook secret、private key、cookie、credential URL、未脫敏 log 或截圖時只能隔離。"},
{"lane_id": "reject_false_green_claim", "meaning": "把 CD success、deploy marker、workflow success、route 200、runner online、UI 可見或 AwoooP approval 當驗收時拒收。"},
{"lane_id": "ready_for_cd_runner_secret_post_incident_review", "meaning": "metadata 合格後,只能進 reviewer review。"},
{"lane_id": "recurrence_guard_backfill_required", "meaning": "需補防再發 guard、owner review、change freeze、automation block 或 runner isolation plan。"},
{"lane_id": "waiting_runtime_gate", "meaning": "即使 readback acceptedruntime gate 仍需獨立人工批准。"},
]
BLOCKED_ACTIONS = [
"modify_workflow",
"workflow_dispatch_without_approval",
"enable_runner",
"change_runner_label",
"install_runner",
"restart_runner",
"use_runner_admin_token",
"enable_github_hosted_runner",
"collect_secret_value",
"collect_secret_hash",
"collect_partial_token",
"collect_masked_token",
"collect_runner_token",
"collect_webhook_secret",
"collect_deploy_key_private_material",
"collect_cookie_or_authorization_header",
"store_raw_workflow_payload",
"store_unredacted_action_log",
"create_repo_secret",
"update_repo_secret",
"rotate_secret",
"delete_secret",
"read_secret_store",
"change_secret_injection_path",
"modify_webhook",
"change_webhook_secret",
"modify_deploy_key",
"rotate_deploy_key",
"change_branch_protection",
"change_codeowners",
"sync_refs",
"force_push",
"switch_github_primary",
"disable_gitea",
"run_cd_pipeline_as_action",
"inject_k8s_secret",
"argocd_sync",
"production_deploy",
"accept_cd_success_as_security_acceptance",
"accept_deploy_marker_as_runtime_approval",
"accept_workflow_success_as_all_green",
"accept_runner_online_as_attestation",
"accept_route_200_as_deploy_acceptance",
"accept_ui_visible_as_runtime_approval",
"skip_log_redaction_review",
"skip_secret_name_parity",
"skip_runner_attestation",
"skip_cross_project_sync",
"skip_rollback_validation",
"skip_post_change_monitoring",
"open_runtime_gate",
"add_action_button",
]
EXECUTION_BOUNDARIES = {
"runtime_execution_authorized": False,
"workflow_modification_authorized": False,
"workflow_dispatch_authorized": False,
"runner_change_authorized": False,
"github_hosted_runner_enable_authorized": False,
"webhook_modification_authorized": False,
"deploy_key_change_authorized": False,
"branch_protection_change_authorized": False,
"codeowners_change_authorized": False,
"repo_secret_change_authorized": False,
"secret_value_collection_allowed": False,
"secret_hash_collection_allowed": False,
"partial_token_collection_allowed": False,
"runner_token_collection_allowed": False,
"webhook_secret_collection_allowed": False,
"deploy_key_private_material_collection_allowed": False,
"secret_rotation_authorized": False,
"secret_store_read_authorized": False,
"secret_injection_change_authorized": False,
"gitea_action_dispatch_authorized": False,
"cd_pipeline_run_authorized": False,
"deploy_marker_write_authorized": False,
"k8s_secret_injection_authorized": False,
"argocd_sync_authorized": False,
"production_deploy_authorized": False,
"refs_sync_authorized": False,
"force_push_authorized": False,
"github_primary_switch_authorized": False,
"disable_gitea_authorized": False,
"action_buttons_allowed": False,
"not_authorization": True,
}
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 build_candidate(source: dict[str, Any]) -> dict[str, Any]:
candidate_id = source["change_evidence_candidate_id"]
return {
"post_incident_readback_candidate_id": f"cd_runner_secret_injection_post_incident_readback:{candidate_id.split(':', 1)[1]}",
"source_change_evidence_candidate_id": candidate_id,
"status": "waiting_post_incident_readback",
"title": source["title"],
"control_tier": source["control_tier"],
"risk": source["risk"],
"source_refs": source["source_refs"],
"affected_scope": source["affected_scope"],
"workflow_secret_name_refs": source.get("workflow_secret_name_refs", []),
"write_capable": source["write_capable"],
"requires_runtime_approval_package": True,
"incident_or_change_ref": None,
"actor_attribution_ref": None,
"change_time_window_ref": None,
"change_intent_or_break_glass_ref": None,
"workflow_diff_state_ref": None,
"runner_attestation_state_ref": None,
"runner_executor_host_readback_ref": None,
"runner_workspace_cleanup_readback_ref": None,
"runner_permission_scope_ref": None,
"secret_name_parity_state_ref": None,
"secret_injection_route_state_ref": None,
"step_env_secret_guard_result_ref": None,
"log_redaction_readback_ref": None,
"deploy_marker_readback_ref": None,
"gitea_action_run_readback_ref": None,
"webhook_delivery_state_ref": None,
"deploy_key_branch_protection_codeowners_ref": None,
"notification_delivery_receipt_ref": None,
"before_after_deploy_state_ref": None,
"affected_route_or_service_state_ref": None,
"cross_project_sync_ref": None,
"rollback_validation_ref": None,
"postcheck_evidence_ref": None,
"post_change_monitoring_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",
"no_secret_value_attestation": False,
"no_raw_workflow_payload_attestation": False,
"no_unredacted_log_attestation": False,
"no_false_green_attestation": False,
"not_approval": True,
"readback_fields": READBACK_FIELDS,
"required_readback_fields": REQUIRED_READBACK_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,
"post_incident_readback_received": False,
"post_incident_readback_accepted": False,
"actor_attribution_accepted": False,
"workflow_diff_state_accepted": False,
"runner_attestation_accepted": False,
"runner_executor_host_readback_accepted": False,
"runner_workspace_cleanup_accepted": False,
"runner_permission_scope_accepted": False,
"secret_name_parity_accepted": False,
"secret_injection_route_accepted": False,
"step_env_secret_guard_accepted": False,
"log_redaction_readback_accepted": False,
"deploy_marker_readback_accepted": False,
"gitea_action_run_readback_accepted": False,
"webhook_delivery_state_accepted": False,
"deploy_key_branch_protection_codeowners_accepted": False,
"notification_delivery_receipt_accepted": False,
"before_after_deploy_state_accepted": False,
"affected_route_or_service_state_accepted": False,
"cross_project_sync_accepted": False,
"rollback_validation_accepted": False,
"postcheck_evidence_accepted": False,
"post_change_monitoring_accepted": False,
"recurrence_guard_accepted": False,
"no_false_green_accepted": False,
"runtime_gate": False,
**{key: value for key, value in EXECUTION_BOUNDARIES.items() if key != "not_authorization"},
}
def build_report(root: Path, generated_at: str | None) -> dict[str, Any]:
source = load_json(
root / "docs/security/cd-runner-secret-injection-change-evidence-acceptance.snapshot.json"
)
source_summary = source["summary"]
candidates = [build_candidate(item) for item in source["change_evidence_candidates"]]
c0_candidates = [item for item in candidates if item["control_tier"] == "C0"]
c1_candidates = [item for item in candidates if item["control_tier"] == "C1"]
report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
return {
"schema_version": "cd_runner_secret_injection_post_incident_readback_plan_v1",
"generated_at": report_time,
"git_commit": git_short_sha(root),
"status": "post_incident_readback_plan_ready_no_runtime_action",
"mode": "metadata_only_no_secret_value_no_workflow_runner_secret_change",
"source_paths": [
"docs/security/cd-runner-secret-injection-change-evidence-acceptance.snapshot.json",
".gitea/workflows/cd.yaml",
".gitea/workflows/code-review.yaml",
".gitea/workflows/deploy-alerts.yaml",
"scripts/ci/check-gitea-step-env-secrets.js",
],
"summary": {
"source_change_evidence_candidate_count": source_summary["change_evidence_candidate_count"],
"source_c0_change_evidence_candidate_count": source_summary["c0_change_evidence_candidate_count"],
"source_c1_change_evidence_candidate_count": source_summary["c1_change_evidence_candidate_count"],
"source_required_evidence_field_count": source_summary["required_evidence_field_count"],
"source_reviewer_check_count": source_summary["reviewer_check_count"],
"source_blocked_action_count": source_summary["blocked_action_count"],
"source_change_evidence_accepted_count": source_summary["change_evidence_accepted_count"],
"source_runtime_gate_count": source_summary["runtime_gate_count"],
"readback_candidate_count": len(candidates),
"c0_readback_candidate_count": len(c0_candidates),
"c1_readback_candidate_count": len(c1_candidates),
"write_capable_readback_candidate_count": sum(1 for item in candidates if item["write_capable"]),
"secret_sensitive_readback_candidate_count": len(candidates),
"runner_or_workflow_readback_candidate_count": len(candidates),
"deploy_or_run_readback_required_candidate_count": len(candidates),
"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,
"workflow_diff_state_accepted_count": 0,
"runner_attestation_accepted_count": 0,
"runner_executor_host_readback_accepted_count": 0,
"runner_workspace_cleanup_accepted_count": 0,
"runner_permission_scope_accepted_count": 0,
"secret_name_parity_accepted_count": 0,
"secret_injection_route_accepted_count": 0,
"step_env_secret_guard_accepted_count": 0,
"log_redaction_readback_accepted_count": 0,
"deploy_marker_readback_accepted_count": 0,
"gitea_action_run_readback_accepted_count": 0,
"webhook_delivery_state_accepted_count": 0,
"deploy_key_branch_protection_codeowners_accepted_count": 0,
"notification_delivery_receipt_accepted_count": 0,
"before_after_deploy_state_accepted_count": 0,
"affected_route_or_service_state_accepted_count": 0,
"cross_project_sync_accepted_count": 0,
"rollback_validation_accepted_count": 0,
"postcheck_evidence_accepted_count": 0,
"post_change_monitoring_accepted_count": 0,
"recurrence_guard_accepted_count": 0,
"no_false_green_accepted_count": 0,
"workflow_modification_authorized_count": 0,
"workflow_dispatch_authorized_count": 0,
"runner_change_authorized_count": 0,
"github_hosted_runner_enable_authorized_count": 0,
"repo_secret_change_authorized_count": 0,
"secret_value_collection_allowed_count": 0,
"secret_injection_change_authorized_count": 0,
"webhook_modification_authorized_count": 0,
"deploy_key_change_authorized_count": 0,
"branch_protection_change_authorized_count": 0,
"codeowners_change_authorized_count": 0,
"gitea_action_dispatch_authorized_count": 0,
"cd_pipeline_run_authorized_count": 0,
"k8s_secret_injection_authorized_count": 0,
"argocd_sync_authorized_count": 0,
"production_deploy_authorized_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
"secret_metadata_coverage_percent_after_readback_plan": 70,
"gitea_workflow_runner_coverage_percent_after_readback_plan": 74,
},
"execution_boundaries": EXECUTION_BOUNDARIES,
"readback_fields": READBACK_FIELDS,
"required_readback_fields": REQUIRED_READBACK_FIELDS,
"reviewer_checks": REVIEWER_CHECKS,
"outcome_lanes": OUTCOME_LANES,
"blocked_actions": BLOCKED_ACTIONS,
"readback_candidates": candidates,
"operator_interpretation": [
"此計畫只描述 CD / runner / secret injection 事故後如何回讀,不是 workflow、runner 或 secret 變更批准。",
"secret 只能以名稱、scope、present-absent、owner 與 redacted evidence ref 呈現,不得保存 value、hash、partial token 或 runner token。",
"CD success、deploy marker、workflow success、route 200、runner online、AwoooP approval 與 UI 可見狀態都不能被解讀成 runtime gate 已開。",
"未來若要修改 workflow、runner、secret injection、webhook、deploy key、branch protection、CODEOWNERS、refs 或 production deploy仍需獨立維護窗口、rollback owner、跨專案同步與 runtime approval package。",
],
}
def main() -> int:
parser = argparse.ArgumentParser(description="CD / Runner / Secret injection 事故後回讀只讀計畫")
parser.add_argument("--root", default=".", help="repo root")
parser.add_argument("--output", help="寫出 JSON 報告")
parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用")
args = parser.parse_args()
root = Path(args.root).resolve()
report = build_report(root, 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(
"CD_RUNNER_SECRET_INJECTION_POST_INCIDENT_READBACK_PLAN_OK "
f"candidates={summary['readback_candidate_count']} "
f"c0={summary['c0_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']}",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())