Files
awoooi/scripts/security/public-runtime-config-change-evidence-acceptance.py
Your Name 5f9a11e6b2
All checks were successful
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m29s
CD Pipeline / build-and-deploy (push) Successful in 4m22s
CD Pipeline / post-deploy-checks (push) Successful in 1m40s
fix(iwooos): 新增 public runtime config 驗收與 tenants 防洩漏
2026-06-15 04:29:54 +08:00

526 lines
21 KiB
Python
Raw Permalink 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 Public / Admin / API runtime config 變更證據驗收只讀帳本產生器。
本工具只整理 repo 內 public route、admin/auth boundary、API/CORS、frontend env、
i18n redaction 與 webhook/callback 的 source refs建立未來 runtime config 變更
證據如何收件、補件、拒收或交給 reviewer 的 metadata-only ledger。它不讀 live
secret、不改 CORS、不改 route、不觸發 webhook、不部署、不開 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))
CHANGE_EVIDENCE_FIELDS = [
"change_evidence_candidate_id",
"source_refs",
"control_tier",
"proposed_runtime_config_change_ref",
"affected_route_refs",
"public_url_or_domain_ref",
"admin_auth_boundary_ref",
"api_contract_readback_ref",
"cors_origin_diff_ref",
"frontend_env_diff_ref",
"i18n_redaction_review_ref",
"webhook_callback_owner_ref",
"desktop_mobile_smoke_ref",
"api_health_readback_ref",
"sensitive_string_scan_ref",
"console_error_scan_ref",
"blast_radius",
"maintenance_window",
"rollback_owner",
"rollback_plan_ref",
"postcheck_evidence_ref",
"redacted_evidence_refs",
"reviewer_outcome",
"not_approval",
]
REQUIRED_EVIDENCE_FIELDS = [
"proposed_runtime_config_change_ref",
"affected_route_refs",
"public_url_or_domain_ref",
"admin_auth_boundary_ref",
"api_contract_readback_ref",
"cors_origin_diff_ref",
"frontend_env_diff_ref",
"i18n_redaction_review_ref",
"webhook_callback_owner_ref",
"desktop_mobile_smoke_ref",
"api_health_readback_ref",
"sensitive_string_scan_ref",
"console_error_scan_ref",
"blast_radius",
"maintenance_window",
"rollback_owner",
"rollback_plan_ref",
"postcheck_evidence_ref",
"redacted_evidence_refs",
"reviewer_outcome",
"not_approval",
]
REVIEWER_CHECKS = [
{
"check_id": "change_ref_present",
"instruction": "必須有 proposed runtime config change ref不能只寫口頭同意。",
},
{
"check_id": "affected_route_refs_present",
"instruction": "必須列出 public / admin / API / callback / webhook / frontend route 影響範圍。",
},
{
"check_id": "public_url_not_internal_ip",
"instruction": "NEXT_PUBLIC 與公開 URL 只能使用 public domain不得暴露內網 IP。",
},
{
"check_id": "admin_auth_boundary_called_out",
"instruction": "涉及後台、operator console 或審批路由時必須標出 auth boundary 與 owner。",
},
{
"check_id": "api_contract_readback_present",
"instruction": "API 變更需有 readback ref且 public payload 不得暴露 raw owner namespace、repo slug 或內部狀態碼。",
},
{
"check_id": "cors_origin_diff_ref_only",
"instruction": "CORS 只能收 origin diff / owner ref不得直接改白名單或使用萬用來源。",
},
{
"check_id": "frontend_env_diff_ref_present",
"instruction": "涉及 frontend env / build-time public config 時必須附 diff ref 與 bundle sensitive scan ref。",
},
{
"check_id": "i18n_redaction_review_present",
"instruction": "前台文案需確認全繁中、無內部對話、無抱怨語句、無 raw identity。",
},
{
"check_id": "webhook_callback_owner_present",
"instruction": "callback / webhook / Sentry tunnel / Telegram route 需有 owner 與回復方式。",
},
{
"check_id": "desktop_mobile_smoke_present",
"instruction": "涉及前台或後台 route 時必須有 desktop / mobile smoke ref 與 horizontal overflow 結果。",
},
{
"check_id": "api_health_readback_present",
"instruction": "API / backend runtime config 需有 health 或 contract readback ref。",
},
{
"check_id": "sensitive_string_scan_present",
"instruction": "必須附 sensitive string scan ref至少檢查 raw namespace、internal state code、internal transcript、secret value。",
},
{
"check_id": "console_error_scan_present",
"instruction": "前端 smoke 需標出 console/page error 結果或說明不適用。",
},
{
"check_id": "no_secret_value_or_cookie",
"instruction": "不得保存 cookie、token、DSN value、secret value、hash、partial token 或 raw payload。",
},
{
"check_id": "security_header_or_cookie_impact_called_out",
"instruction": "若影響 headers、cookie、CSRF、rate limit 或 middleware必須標出安全影響。",
},
{
"check_id": "blast_radius_present",
"instruction": "必須列出產品、route、API、admin/auth、public domain、callback、webhook 與使用者影響。",
},
{
"check_id": "maintenance_window_present",
"instruction": "任何 future runtime config 變更都必須另有維護窗口或明確 not-applicable 理由。",
},
{
"check_id": "rollback_owner_present",
"instruction": "必須有 rollback owner 與回復方式;不能只寫『可回復』。",
},
{
"check_id": "postcheck_evidence_present",
"instruction": "需有 post-check evidence ref例如 API readback、browser smoke、bundle scan 或 alert silence review。",
},
{
"check_id": "no_runtime_action_claim",
"instruction": "不能把本帳本、UI 可見、CD success、AwoooP approval 或 smoke pass 當資安批准。",
},
{
"check_id": "cross_project_sync_noted",
"instruction": "若影響 AwoooP、IwoooS、agent-bounty、StockPlatform、公開網站或監控需有跨專案同步 ref。",
},
]
OUTCOME_LANES = [
{
"lane_id": "waiting_change_evidence",
"meaning": "尚未收到 runtime config 變更證據;所有 accepted / runtime count 維持 0。",
},
{
"lane_id": "quarantine_sensitive_payload",
"meaning": "收到 cookie、token、secret value、raw internal payload 或未脫敏截圖時只能隔離。",
},
{
"lane_id": "reject_unredacted_or_runtime_claim",
"meaning": "出現 raw identity、internal transcript、internal state code 或把 evidence 誤當批准時直接拒收。",
},
{
"lane_id": "request_supplement",
"meaning": "缺 route scope、auth boundary、CORS diff、desktop/mobile smoke、rollback 或 post-check 時要求補件。",
},
{
"lane_id": "ready_for_reviewer_acceptance",
"meaning": "metadata 合格後只能進 reviewer acceptance不得自動改 route / CORS / env。",
},
{
"lane_id": "ready_for_runtime_approval_package",
"meaning": "reviewer 接受後也只能形成 runtime approval package不自動打開 gate。",
},
{
"lane_id": "waiting_maintenance_window",
"meaning": "若未來要改 public/admin/API runtime config仍需獨立維護窗口。",
},
{
"lane_id": "waiting_runtime_gate",
"meaning": "change evidence accepted 後 runtime gate 仍等待獨立人工批准。",
},
]
BLOCKED_ACTIONS = [
"change_public_route",
"change_admin_route",
"change_api_route",
"change_cors_origin",
"modify_next_public_env",
"expose_internal_ip",
"expose_repo_slug",
"expose_owner_namespace",
"expose_secret_value",
"bypass_auth",
"change_callback_url",
"change_webhook_secret",
"modify_middleware_auth",
"disable_csrf",
"disable_rate_limit",
"change_cookie_policy",
"change_security_headers",
"publish_internal_transcript",
"publish_internal_status_code",
"deploy_frontend",
"deploy_api",
"rewrite_nginx_route",
"change_public_url",
"change_openapi_contract",
"mutate_database",
"run_migration",
"send_webhook",
"active_scan",
"enable_action_button",
"production_deploy",
"force_push",
"switch_github_primary",
]
EXECUTION_BOUNDARIES = {
"runtime_execution_authorized": False,
"runtime_config_change_authorized": False,
"public_route_change_authorized": False,
"admin_route_change_authorized": False,
"api_route_change_authorized": False,
"cors_change_authorized": False,
"frontend_env_change_authorized": False,
"middleware_auth_change_authorized": False,
"callback_url_change_authorized": False,
"webhook_receiver_change_authorized": False,
"webhook_secret_change_authorized": False,
"security_header_change_authorized": False,
"cookie_policy_change_authorized": False,
"csrf_disable_authorized": False,
"rate_limit_disable_authorized": False,
"api_contract_change_authorized": False,
"i18n_public_text_internal_identity_allowed": False,
"internal_ip_exposure_allowed": False,
"repo_namespace_exposure_allowed": False,
"owner_namespace_exposure_allowed": False,
"internal_status_code_exposure_allowed": False,
"internal_transcript_exposure_allowed": False,
"secret_value_collection_allowed": False,
"secret_hash_collection_allowed": False,
"partial_token_collection_allowed": False,
"raw_payload_storage_allowed": False,
"desktop_mobile_smoke_authorized": False,
"route_smoke_authorized": False,
"production_deploy_authorized": False,
"database_migration_authorized": False,
"force_push_authorized": False,
"github_primary_switch_authorized": False,
"action_buttons_allowed": False,
"not_authorization": True,
}
CANDIDATE_DEFINITIONS = [
{
"candidate_id": "public_product_route_and_i18n_redaction",
"title": "Public product route / i18n redaction boundary",
"control_tier": "C0",
"risk": "HIGH",
"source_refs": [
"apps/web/src/app/[locale]",
"apps/web/messages/zh-TW.json",
"apps/web/messages/en.json",
],
"affected_scope": "公開產品頁、IwoooS / AwoooP / Tenants / Code Review 前台文案、raw identity 與內部協作文字防外洩",
},
{
"candidate_id": "admin_auth_and_operator_console_boundary",
"title": "Admin / operator console auth boundary",
"control_tier": "C0",
"risk": "HIGH",
"source_refs": [
"apps/web/src/app/[locale]/awooop",
"apps/api/src/core/awooop_operator_auth.py",
"apps/api/src/core/csrf.py",
],
"affected_scope": "AwoooP operator console、approvals、work-items、runs、admin auth / CSRF / owner guard",
},
{
"candidate_id": "api_cors_and_public_url_runtime_config",
"title": "API / CORS / public URL runtime config",
"control_tier": "C0",
"risk": "HIGH",
"source_refs": [
"apps/api/src/core/config.py",
"apps/api/src/config.py",
"apps/web/src/lib/config.ts",
"apps/web/src/lib/api-client.ts",
],
"affected_scope": "API base URL、CORS origins、NEXT_PUBLIC build-time config、public domain / internal IP boundary",
},
{
"candidate_id": "frontend_env_and_sentry_tunnel_runtime_config",
"title": "Frontend env / Sentry tunnel / browser runtime config",
"control_tier": "C0",
"risk": "HIGH",
"source_refs": [
"apps/web/src/middleware.ts",
"apps/web/src/app/api/sentry-tunnel/route.ts",
"apps/web/src/app/api/health/route.ts",
],
"affected_scope": "Next.js middleware、Sentry tunnel、browser-facing env、health route 與 console error boundary",
},
{
"candidate_id": "webhook_callback_and_notification_runtime_config",
"title": "Webhook / callback / notification runtime route",
"control_tier": "C0",
"risk": "HIGH",
"source_refs": [
"apps/api/src/models/webhook.py",
"apps/api/src/routers/proposals.py",
"apps/api/src/core/deep_linking.py",
],
"affected_scope": "webhook callback、proposal route、deep link、notification route 與 external send boundary",
},
{
"candidate_id": "cross_product_runtime_route_scope",
"title": "Cross-product runtime route scope",
"control_tier": "C1",
"risk": "MEDIUM",
"source_refs": [
"docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md",
"docs/security/VIBEWORK-IWOOOS-ONBOARDING-HANDOFF.md",
"docs/security/AGENT-BOUNTY-IWOOOS-ONBOARDING-HANDOFF.md",
"docs/security/AGENT-BOUNTY-OWNER-REQUEST-DRAFT.md",
],
"affected_scope": "VibeWork、agent-bounty-protocol、StockPlatform、官方形象網站、藥局網站與其他產品 runtime route scope",
},
]
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 source_ref_exists(root: Path, ref: str) -> bool:
return (root / ref).exists()
def build_candidate(root: Path, definition: dict[str, Any]) -> dict[str, Any]:
source_refs = list(definition["source_refs"])
return {
"change_evidence_candidate_id": f"public_runtime_config_change_evidence:{definition['candidate_id']}",
"status": "waiting_change_evidence",
"title": definition["title"],
"control_tier": definition["control_tier"],
"risk": definition["risk"],
"source_refs": source_refs,
"source_ref_existing_count": sum(1 for ref in source_refs if source_ref_exists(root, ref)),
"write_capable": True,
"requires_runtime_approval_package": True,
"affected_scope": definition["affected_scope"],
"change_evidence_fields": CHANGE_EVIDENCE_FIELDS,
"required_evidence_fields": REQUIRED_EVIDENCE_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,
"proposed_runtime_config_change_ref": None,
"affected_route_refs": [],
"public_url_or_domain_ref": None,
"admin_auth_boundary_ref": None,
"api_contract_readback_ref": None,
"cors_origin_diff_ref": None,
"frontend_env_diff_ref": None,
"i18n_redaction_review_ref": None,
"webhook_callback_owner_ref": None,
"desktop_mobile_smoke_ref": None,
"api_health_readback_ref": None,
"sensitive_string_scan_ref": None,
"console_error_scan_ref": None,
"blast_radius": "pending_change_evidence",
"maintenance_window": "pending_change_evidence",
"rollback_owner": "pending_change_evidence",
"rollback_plan_ref": None,
"postcheck_evidence_ref": None,
"redacted_evidence_refs": [],
"reviewer_outcome": "waiting_change_evidence",
"not_approval": True,
"change_evidence_received": False,
"change_evidence_accepted": False,
"change_evidence_rejected": False,
"change_evidence_quarantined": False,
"route_scope_accepted": False,
"admin_auth_boundary_accepted": False,
"api_contract_readback_accepted": False,
"cors_origin_diff_accepted": False,
"frontend_env_diff_accepted": False,
"i18n_redaction_review_accepted": False,
"webhook_callback_owner_accepted": False,
"desktop_mobile_smoke_accepted": False,
"sensitive_string_scan_accepted": False,
"postcheck_evidence_accepted": False,
"runtime_approval_package_ready": 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]:
report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
candidates = [build_candidate(root, item) for item in CANDIDATE_DEFINITIONS]
c0_candidates = [item for item in candidates if item["control_tier"] == "C0"]
c1_candidates = [item for item in candidates if item["control_tier"] == "C1"]
source_refs = sorted({ref for item in candidates for ref in item["source_refs"]})
return {
"schema_version": "public_runtime_config_change_evidence_acceptance_v1",
"generated_at": report_time,
"git_commit": git_short_sha(root),
"status": "change_evidence_acceptance_ledger_ready_no_runtime_action",
"mode": "metadata_only_no_secret_no_route_change_no_deploy",
"source_paths": [
"docs/HARD_RULES.md",
"docs/security/IWOOOS-CONFIG-CONTROL-INVENTORY.md",
*source_refs,
],
"summary": {
"change_evidence_candidate_count": len(candidates),
"c0_change_evidence_candidate_count": len(c0_candidates),
"c1_change_evidence_candidate_count": len(c1_candidates),
"write_capable_candidate_count": sum(1 for item in candidates if item["write_capable"]),
"source_ref_count": len(source_refs),
"existing_source_ref_count": sum(1 for ref in source_refs if source_ref_exists(root, ref)),
"required_evidence_field_count": len(REQUIRED_EVIDENCE_FIELDS),
"reviewer_check_count": len(REVIEWER_CHECKS),
"outcome_lane_count": len(OUTCOME_LANES),
"blocked_action_count": len(BLOCKED_ACTIONS),
"change_evidence_received_count": 0,
"change_evidence_accepted_count": 0,
"route_scope_accepted_count": 0,
"admin_auth_boundary_accepted_count": 0,
"api_contract_readback_accepted_count": 0,
"cors_origin_diff_accepted_count": 0,
"frontend_env_diff_accepted_count": 0,
"i18n_redaction_review_accepted_count": 0,
"webhook_callback_owner_accepted_count": 0,
"desktop_mobile_smoke_accepted_count": 0,
"sensitive_string_scan_accepted_count": 0,
"postcheck_evidence_accepted_count": 0,
"runtime_approval_package_ready_count": 0,
"runtime_gate_count": 0,
"action_button_count": 0,
"public_admin_api_runtime_config_coverage_percent_before_acceptance": 62,
"public_admin_api_runtime_config_coverage_percent_after_acceptance": 64,
},
"execution_boundaries": EXECUTION_BOUNDARIES,
"change_evidence_fields": CHANGE_EVIDENCE_FIELDS,
"required_evidence_fields": REQUIRED_EVIDENCE_FIELDS,
"reviewer_checks": REVIEWER_CHECKS,
"outcome_lanes": OUTCOME_LANES,
"blocked_actions": BLOCKED_ACTIONS,
"change_evidence_candidates": candidates,
"operator_interpretation": [
"此帳本只描述 public/admin/API/frontend runtime config 變更證據如何收件與拒收,不是 route、CORS、env、auth 或 webhook 變更批准。",
"前台、public API、HTML、bundle 與 messages 不得顯示 raw owner namespace、repo slug、內部狀態碼、內部對話或 secret value。",
"desktop/mobile smoke、API health、CD success、UI 可見與 AwoooP approval 都不能被解讀成 runtime gate 已開。",
"未來若要修改 public/admin/API route、CORS、NEXT_PUBLIC env、middleware auth、callback 或 webhook仍需獨立 owner response、維護窗口、rollback owner 與 runtime approval package。",
],
}
def main() -> int:
parser = argparse.ArgumentParser(description="Public / Admin / API runtime config 變更證據驗收只讀帳本")
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(
"PUBLIC_RUNTIME_CONFIG_CHANGE_EVIDENCE_ACCEPTANCE_OK "
f"candidates={summary['change_evidence_candidate_count']} "
f"c0={summary['c0_change_evidence_candidate_count']} "
f"write_capable={summary['write_capable_candidate_count']} "
f"checks={summary['reviewer_check_count']} "
f"lanes={summary['outcome_lane_count']} "
f"accepted={summary['change_evidence_accepted_count']} "
f"runtime_gate={summary['runtime_gate_count']}",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())