From 5d6b12885429fa37cd21d2e7c8b07f81caa130d3 Mon Sep 17 00:00:00 2001 From: ogt Date: Fri, 26 Jun 2026 17:45:00 +0800 Subject: [PATCH] feat(security): add GitHub private backup evidence gate --- apps/api/src/api/v1/agents.py | 30 + ...hub_target_private_backup_evidence_gate.py | 192 +++++++ ...hub_target_private_backup_evidence_gate.py | 171 ++++++ ...target_private_backup_evidence_gate_api.py | 44 ++ apps/web/src/lib/api-client.ts | 86 +++ docs/LOGBOOK.md | 75 +++ ...rivate_backup_evidence_gate_v1.schema.json | 160 ++++++ ...HUB-TARGET-PRIVATE-BACKUP-EVIDENCE-GATE.md | 32 ++ ...private-backup-evidence-gate.snapshot.json | 533 ++++++++++++++++++ ...hub-target-private-backup-evidence-gate.py | 298 ++++++++++ 10 files changed, 1621 insertions(+) create mode 100644 apps/api/src/services/github_target_private_backup_evidence_gate.py create mode 100644 apps/api/tests/test_github_target_private_backup_evidence_gate.py create mode 100644 apps/api/tests/test_github_target_private_backup_evidence_gate_api.py create mode 100644 docs/schemas/github_target_private_backup_evidence_gate_v1.schema.json create mode 100644 docs/security/GITHUB-TARGET-PRIVATE-BACKUP-EVIDENCE-GATE.md create mode 100644 docs/security/github-target-private-backup-evidence-gate.snapshot.json create mode 100644 scripts/security/github-target-private-backup-evidence-gate.py diff --git a/apps/api/src/api/v1/agents.py b/apps/api/src/api/v1/agents.py index 8b8e9aeb..4531ab2e 100644 --- a/apps/api/src/api/v1/agents.py +++ b/apps/api/src/api/v1/agents.py @@ -316,6 +316,9 @@ from src.services.docker_build_surface_inventory import ( from src.services.gitea_workflow_runner_health import ( load_latest_gitea_workflow_runner_health, ) +from src.services.github_target_private_backup_evidence_gate import ( + load_latest_github_target_private_backup_evidence_gate, +) from src.services.host_runaway_aiops_loop_readiness import ( load_latest_host_runaway_aiops_loop_readiness, ) @@ -3109,6 +3112,33 @@ async def get_gitea_workflow_runner_health() -> dict[str, Any]: ) from exc +@router.get( + "/github-target-private-backup-evidence-gate", + response_model=dict[str, Any], + summary="取得 GitHub 私有備援 evidence gate", + description=( + "讀取最新已提交的 GitHub target private backup evidence gate;" + "此端點不呼叫 GitHub / Gitea API、不建立 repo、不修改 visibility、" + "不同步 refs、不觸發 workflow、不切 GitHub primary、不讀取或保存 secret value。" + ), +) +async def get_github_target_private_backup_evidence_gate() -> dict[str, Any]: + """Return the latest read-only GitHub private backup evidence gate.""" + try: + return await asyncio.to_thread(load_latest_github_target_private_backup_evidence_gate) + except FileNotFoundError as exc: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=str(exc), + ) from exc + except (json.JSONDecodeError, ValueError) as exc: + logger.error("github_target_private_backup_evidence_gate_invalid", error=str(exc)) + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="GitHub 私有備援 evidence gate 快照無效", + ) from exc + + @router.get( "/observability-contract-matrix", response_model=dict[str, Any], diff --git a/apps/api/src/services/github_target_private_backup_evidence_gate.py b/apps/api/src/services/github_target_private_backup_evidence_gate.py new file mode 100644 index 00000000..c9118ed3 --- /dev/null +++ b/apps/api/src/services/github_target_private_backup_evidence_gate.py @@ -0,0 +1,192 @@ +"""GitHub target private backup evidence gate snapshot loader. + +Loads the committed, read-only GitHub private backup evidence gate. This module +never calls GitHub / Gitea, creates repos, changes visibility, syncs refs, +triggers workflows, switches primary source control, or reads secret values. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from src.services.snapshot_paths import resolve_repo_root + +_DEFAULT_SECURITY_DIR = resolve_repo_root(Path(__file__)) / "docs" / "security" +_SNAPSHOT_NAME = "github-target-private-backup-evidence-gate.snapshot.json" +_SCHEMA_VERSION = "github_target_private_backup_evidence_gate_v1" + + +def load_latest_github_target_private_backup_evidence_gate( + security_dir: Path | None = None, +) -> dict[str, Any]: + """Load the committed GitHub private backup evidence gate snapshot.""" + directory = security_dir or _DEFAULT_SECURITY_DIR + latest = directory / _SNAPSHOT_NAME + with latest.open(encoding="utf-8") as handle: + payload = json.load(handle) + + if not isinstance(payload, dict): + raise ValueError(f"{latest}: expected JSON object") + _require_schema(payload, str(latest)) + _require_summary_consistency(payload, str(latest)) + _require_fail_closed_boundaries(payload, str(latest)) + _require_target_gate_consistency(payload, str(latest)) + _require_no_secret_payload_keys(payload, str(latest)) + return payload + + +def _require_schema(payload: dict[str, Any], label: str) -> None: + actual = payload.get("schema_version") + if actual != _SCHEMA_VERSION: + raise ValueError(f"{label}: expected schema_version={_SCHEMA_VERSION}, got {actual!r}") + + +def _require_summary_consistency(payload: dict[str, Any], label: str) -> None: + summary = payload.get("summary") or {} + targets = payload.get("targets") or [] + approval_targets = [target for target in targets if target.get("approval_required") is True] + public_visible = [ + target + for target in approval_targets + if str(target.get("probe_status") or "").startswith("exists") + ] + not_found_or_private = [ + target + for target in approval_targets + if target.get("probe_status") == "not_found_or_private" + ] + + expected = { + "target_decision_count": len(targets), + "approval_required_target_count": len(approval_targets), + "public_probe_visible_target_count": len(public_visible), + "not_found_or_private_target_count": len(not_found_or_private), + "private_backup_verified_count": 0, + "private_visibility_evidence_missing_count": len(approval_targets), + "safe_credential_required_count": len(approval_targets), + "safe_credential_accepted_evidence_count": 0, + "execution_ready_count": 0, + "blocked_target_count": len(approval_targets), + } + mismatches = { + key: {"expected": value, "actual": summary.get(key)} + for key, value in expected.items() + if summary.get(key) != value + } + if mismatches: + raise ValueError(f"{label}: summary mismatch {mismatches}") + + if summary.get("public_repo_allowed") is not False: + raise ValueError(f"{label}: public_repo_allowed must stay false") + if summary.get("not_found_or_private_as_absent_allowed") is not False: + raise ValueError(f"{label}: not_found_or_private_as_absent_allowed must stay false") + + +def _require_fail_closed_boundaries(payload: dict[str, Any], label: str) -> None: + boundaries = payload.get("operation_boundaries") or {} + if boundaries.get("read_only_api_allowed") is not True: + raise ValueError(f"{label}: read_only_api_allowed must be true") + + blocked_operation_flags = { + "github_api_write_allowed", + "gitea_api_write_allowed", + "repo_creation_allowed", + "visibility_change_allowed", + "refs_sync_allowed", + "workflow_modification_allowed", + "workflow_trigger_allowed", + "github_primary_switch_allowed", + "secret_value_collection_allowed", + "private_clone_url_collection_allowed", + } + allowed_operations = sorted( + flag for flag in blocked_operation_flags if boundaries.get(flag) is not False + ) + if allowed_operations: + raise ValueError(f"{label}: operation boundaries must remain false: {allowed_operations}") + + flags = payload.get("authorization_flags") or {} + allowed_flags = sorted(flag for flag, value in flags.items() if value is not False) + if allowed_flags: + raise ValueError(f"{label}: authorization flags must remain false: {allowed_flags}") + + summary = payload.get("summary") or {} + false_summary_flags = { + "repo_creation_authorized", + "visibility_change_authorized", + "refs_sync_authorized", + "github_primary_switch_authorized", + "workflow_modification_authorized", + "workflow_trigger_authorized", + "secret_value_collection_allowed", + "private_clone_url_collection_allowed", + } + allowed_summary = sorted(flag for flag in false_summary_flags if summary.get(flag) is not False) + if allowed_summary: + raise ValueError(f"{label}: summary authorization flags must remain false: {allowed_summary}") + + +def _require_target_gate_consistency(payload: dict[str, Any], label: str) -> None: + targets = payload.get("targets") or [] + for target in targets: + if target.get("approval_required") is not True: + continue + repo = target.get("github_repo") + if target.get("private_backup_verified") is not False: + raise ValueError(f"{label}: {repo} private_backup_verified must stay false") + if target.get("refs_sync_ready") is not False or target.get("execution_ready") is not False: + raise ValueError(f"{label}: {repo} refs_sync_ready/execution_ready must stay false") + if not target.get("blockers"): + raise ValueError(f"{label}: {repo} must keep blockers until owner evidence is accepted") + false_flags = { + "repo_creation_authorized", + "visibility_change_authorized", + "refs_sync_authorized", + "github_primary_switch_authorized", + "secret_values_collected", + } + allowed = sorted(flag for flag in false_flags if target.get(flag) is not False) + if allowed: + raise ValueError(f"{label}: {repo} target flags must remain false: {allowed}") + + status = str(target.get("visibility_evidence_status") or "") + probe_status = str(target.get("probe_status") or "") + if probe_status.startswith("exists") and "public_probe_visible" not in status: + raise ValueError(f"{label}: {repo} public probe visibility must be blocked") + if probe_status == "not_found_or_private" and "not_verified" not in status: + raise ValueError(f"{label}: {repo} not_found_or_private must not be private verification") + + +def _require_no_secret_payload_keys(payload: Any, label: str, path: str = "$") -> None: + forbidden_fragments = { + "token", + "secret", + "private_key", + "cookie", + "session", + "credential", + "authorization", + "clone_url", + } + if isinstance(payload, dict): + for key, value in payload.items(): + normalized = str(key).lower() + if any(fragment in normalized for fragment in forbidden_fragments): + if normalized not in { + "secret_value_collection_allowed", + "secret_values_collected", + "private_clone_url_collection_allowed", + "safe_credential_evidence_status", + "safe_credential_evidence_ref", + "safe_credential_required_count", + "safe_credential_accepted_evidence_count", + "forbidden_action_count", + "authorization_flags", + }: + raise ValueError(f"{label}: forbidden secret payload key at {path}.{key}") + _require_no_secret_payload_keys(value, label, f"{path}.{key}") + elif isinstance(payload, list): + for index, value in enumerate(payload): + _require_no_secret_payload_keys(value, label, f"{path}[{index}]") diff --git a/apps/api/tests/test_github_target_private_backup_evidence_gate.py b/apps/api/tests/test_github_target_private_backup_evidence_gate.py new file mode 100644 index 00000000..0c624bf6 --- /dev/null +++ b/apps/api/tests/test_github_target_private_backup_evidence_gate.py @@ -0,0 +1,171 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from src.services.github_target_private_backup_evidence_gate import ( + load_latest_github_target_private_backup_evidence_gate, +) + + +def test_load_latest_github_target_private_backup_evidence_gate_reads_committed_snapshot(): + data = load_latest_github_target_private_backup_evidence_gate() + + assert data["schema_version"] == "github_target_private_backup_evidence_gate_v1" + assert data["status"] == "blocked_public_visibility_and_safe_credential_evidence_required" + assert data["summary"]["approval_required_target_count"] == 9 + assert data["summary"]["public_probe_visible_target_count"] == 4 + assert data["summary"]["not_found_or_private_target_count"] == 5 + assert data["summary"]["private_backup_verified_count"] == 0 + assert data["summary"]["safe_credential_accepted_evidence_count"] == 0 + assert data["summary"]["safe_credential_required_count"] == 9 + assert data["summary"]["refs_sync_authorized"] is False + assert data["summary"]["public_repo_allowed"] is False + assert data["operation_boundaries"]["read_only_api_allowed"] is True + assert data["operation_boundaries"]["repo_creation_allowed"] is False + assert data["authorization_flags"]["repo_creation_authorized"] is False + assert data["authorization_flags"]["secret_values_collected"] is False + + +def test_private_backup_gate_rejects_runtime_authorization(tmp_path: Path): + snapshot = _snapshot() + snapshot["authorization_flags"]["refs_sync_authorized"] = True + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="authorization flags"): + load_latest_github_target_private_backup_evidence_gate(tmp_path) + + +def test_private_backup_gate_rejects_summary_drift(tmp_path: Path): + snapshot = _snapshot() + snapshot["summary"]["private_backup_verified_count"] = 1 + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="summary mismatch"): + load_latest_github_target_private_backup_evidence_gate(tmp_path) + + +def test_private_backup_gate_rejects_public_target_marked_private(tmp_path: Path): + snapshot = _snapshot() + snapshot["targets"][0]["private_backup_verified"] = True + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="private_backup_verified"): + load_latest_github_target_private_backup_evidence_gate(tmp_path) + + +def test_private_backup_gate_rejects_not_found_as_verified(tmp_path: Path): + snapshot = _snapshot() + snapshot["targets"][1]["visibility_evidence_status"] = "private_verified" + _write_snapshot(tmp_path, snapshot) + + with pytest.raises(ValueError, match="not_found_or_private"): + load_latest_github_target_private_backup_evidence_gate(tmp_path) + + +def _write_snapshot(directory: Path, snapshot: dict) -> None: + (directory / "github-target-private-backup-evidence-gate.snapshot.json").write_text( + json.dumps(snapshot), + encoding="utf-8", + ) + + +def _snapshot() -> dict: + return { + "schema_version": "github_target_private_backup_evidence_gate_v1", + "generated_at": "2026-06-26T00:00:00+00:00", + "status": "blocked_public_visibility_and_safe_credential_evidence_required", + "mode": "read_only_private_backup_evidence_gate", + "source_reviews": {}, + "summary": { + "target_decision_count": 3, + "approval_required_target_count": 2, + "approval_package_item_count": 2, + "public_probe_visible_target_count": 1, + "not_found_or_private_target_count": 1, + "private_backup_verified_count": 0, + "private_visibility_evidence_missing_count": 2, + "safe_credential_required_count": 2, + "safe_credential_accepted_evidence_count": 0, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "execution_ready_count": 0, + "blocked_target_count": 2, + "external_scope_target_count": 1, + "forbidden_action_count": 12, + "repo_creation_authorized": False, + "visibility_change_authorized": False, + "refs_sync_authorized": False, + "github_primary_switch_authorized": False, + "workflow_modification_authorized": False, + "workflow_trigger_authorized": False, + "secret_value_collection_allowed": False, + "private_clone_url_collection_allowed": False, + "not_found_or_private_as_absent_allowed": False, + "public_repo_allowed": False, + }, + "targets": [ + _target("owenhytsai/awoooi", True, "exists", "blocked_public_probe_visible_private_evidence_required"), + _target("owenhytsai/VibeWork", True, "not_found_or_private", "blocked_private_or_absent_not_verified"), + _target("nexu-io/open-design", False, "exists", "external_scope_not_backup_target"), + ], + "acceptance_requirements": ["private evidence required"], + "rejection_rules": ["reject secrets"], + "operation_boundaries": { + "read_only_api_allowed": True, + "github_api_write_allowed": False, + "gitea_api_write_allowed": False, + "repo_creation_allowed": False, + "visibility_change_allowed": False, + "refs_sync_allowed": False, + "workflow_modification_allowed": False, + "workflow_trigger_allowed": False, + "github_primary_switch_allowed": False, + "secret_value_collection_allowed": False, + "private_clone_url_collection_allowed": False, + }, + "authorization_flags": { + "runtime_execution_authorized": False, + "repo_creation_authorized": False, + "visibility_change_authorized": False, + "refs_sync_authorized": False, + "workflow_modification_authorized": False, + "workflow_trigger_authorized": False, + "github_primary_switch_authorized": False, + "secret_values_collected": False, + }, + } + + +def _target( + repo: str, + approval_required: bool, + probe_status: str, + visibility_status: str, +) -> dict: + return { + "github_repo": repo, + "source_key": repo, + "approval_required": approval_required, + "probe_status": probe_status, + "target_state": probe_status, + "risk": "HIGH", + "visibility_evidence_status": visibility_status, + "private_backup_verified": False, + "private_visibility_owner_evidence_ref": None, + "safe_credential_evidence_status": "missing_safe_credential_metadata", + "safe_credential_evidence_ref": None, + "owner_response_accepted": False, + "refs_sync_ready": False, + "execution_ready": False, + "blockers": ["blocked"], + "evidence_refs": ["docs/security/github-target-decision.snapshot.json"], + "forbidden_actions": ["push_refs"], + "repo_creation_authorized": False, + "visibility_change_authorized": False, + "refs_sync_authorized": False, + "github_primary_switch_authorized": False, + "secret_values_collected": False, + } diff --git a/apps/api/tests/test_github_target_private_backup_evidence_gate_api.py b/apps/api/tests/test_github_target_private_backup_evidence_gate_api.py new file mode 100644 index 00000000..d24b91da --- /dev/null +++ b/apps/api/tests/test_github_target_private_backup_evidence_gate_api.py @@ -0,0 +1,44 @@ +from __future__ import annotations + +from fastapi import FastAPI +from fastapi.testclient import TestClient + +from src.api.v1.agents import router + + +def test_github_target_private_backup_evidence_gate_endpoint_returns_committed_snapshot(): + app = FastAPI() + app.include_router(router, prefix="/api/v1") + client = TestClient(app) + + response = client.get("/api/v1/agents/github-target-private-backup-evidence-gate") + + assert response.status_code == 200 + data = response.json() + assert data["schema_version"] == "github_target_private_backup_evidence_gate_v1" + assert data["status"] == "blocked_public_visibility_and_safe_credential_evidence_required" + assert data["summary"]["approval_required_target_count"] == 9 + assert data["summary"]["public_probe_visible_target_count"] == 4 + assert data["summary"]["not_found_or_private_target_count"] == 5 + assert data["summary"]["private_backup_verified_count"] == 0 + assert data["summary"]["safe_credential_accepted_evidence_count"] == 0 + assert data["summary"]["execution_ready_count"] == 0 + assert data["summary"]["public_repo_allowed"] is False + assert data["operation_boundaries"]["read_only_api_allowed"] is True + assert data["operation_boundaries"]["repo_creation_allowed"] is False + assert data["operation_boundaries"]["refs_sync_allowed"] is False + assert data["operation_boundaries"]["workflow_trigger_allowed"] is False + assert data["authorization_flags"]["runtime_execution_authorized"] is False + assert data["authorization_flags"]["repo_creation_authorized"] is False + assert data["authorization_flags"]["secret_values_collected"] is False + + public_target = next(target for target in data["targets"] if target["github_repo"] == "owenhytsai/awoooi") + assert public_target["visibility_evidence_status"] == ( + "blocked_public_probe_visible_private_evidence_required" + ) + private_or_absent_target = next( + target for target in data["targets"] if target["github_repo"] == "owenhytsai/VibeWork" + ) + assert private_or_absent_target["visibility_evidence_status"] == ( + "blocked_private_or_absent_not_verified" + ) diff --git a/apps/web/src/lib/api-client.ts b/apps/web/src/lib/api-client.ts index bdee5bac..aca8e2f7 100644 --- a/apps/web/src/lib/api-client.ts +++ b/apps/web/src/lib/api-client.ts @@ -704,6 +704,11 @@ export const apiClient = { return handleResponse(res) }, + async getGithubTargetPrivateBackupEvidenceGate() { + const res = await fetch(`${API_BASE_URL}/agents/github-target-private-backup-evidence-gate`) + return handleResponse(res) + }, + async getObservabilityContractMatrix() { const res = await fetch(`${API_BASE_URL}/agents/observability-contract-matrix`) return handleResponse(res) @@ -12587,6 +12592,87 @@ export interface GiteaWorkflowRunnerHealthSnapshot { approval_boundaries: Record } +export interface GithubTargetPrivateBackupEvidenceGateSnapshot { + schema_version: 'github_target_private_backup_evidence_gate_v1' + generated_at: string + status: + | 'blocked_public_visibility_and_safe_credential_evidence_required' + | 'blocked_private_visibility_and_safe_credential_evidence_required' + mode: 'read_only_private_backup_evidence_gate' + source_reviews: Record + summary: { + target_decision_count: number + approval_required_target_count: number + approval_package_item_count: number + public_probe_visible_target_count: number + not_found_or_private_target_count: number + private_backup_verified_count: number + private_visibility_evidence_missing_count: number + safe_credential_required_count: number + safe_credential_accepted_evidence_count: number + owner_response_received_count: number + owner_response_accepted_count: number + execution_ready_count: number + blocked_target_count: number + external_scope_target_count: number + forbidden_action_count: number + repo_creation_authorized: false + visibility_change_authorized: false + refs_sync_authorized: false + github_primary_switch_authorized: false + workflow_modification_authorized: false + workflow_trigger_authorized: false + secret_value_collection_allowed: false + private_clone_url_collection_allowed: false + not_found_or_private_as_absent_allowed: false + public_repo_allowed: false + } + targets: Array<{ + github_repo: string + source_key: string + approval_required: boolean + probe_status: string + target_state: string + risk: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL' | string + visibility_evidence_status: + | 'external_scope_not_backup_target' + | 'blocked_public_probe_visible_private_evidence_required' + | 'blocked_private_or_absent_not_verified' + | 'blocked_probe_status_unknown' + private_backup_verified: false + private_visibility_owner_evidence_ref: string | null + safe_credential_evidence_status: string + safe_credential_evidence_ref: string | null + owner_response_accepted: false + refs_sync_ready: false + execution_ready: false + blockers: string[] + evidence_refs: string[] + forbidden_actions: string[] + repo_creation_authorized: false + visibility_change_authorized: false + refs_sync_authorized: false + github_primary_switch_authorized: false + secret_values_collected: false + }> + acceptance_requirements: string[] + rejection_rules: string[] + operation_boundaries: { + read_only_api_allowed: true + github_api_write_allowed: false + gitea_api_write_allowed: false + repo_creation_allowed: false + visibility_change_allowed: false + refs_sync_allowed: false + workflow_modification_allowed: false + workflow_trigger_allowed: false + github_primary_switch_allowed: false + secret_value_collection_allowed: false + private_clone_url_collection_allowed: false + } + authorization_flags: Record +} + export interface ObservabilityContractMatrixSnapshot { schema_version: 'observability_contract_matrix_v1' generated_at: string diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 336c5c99..7f8234ba 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,78 @@ +## 2026-06-26|GitHub 私有備援 evidence gate / API client 收斂 + +**整體完成度**:依 `~/.codex/CODEX-START-HERE.md` 與 `product-runtime-governance-completion-scorecard`,全產品治理總工程仍為 `42.2%`、`not_complete`。本段只把 GitHub 備援鏡像的「私有性證據與安全 credential 前置 gate」補成 source-side snapshot、API service、API route、前端 client 與測試;不調高 GitHub mirror 實際完成度,也不宣稱任何專案已推上 GitHub。 + +**受控工作區**: +- path:`/Users/ogt/codex-workspaces/awoooi-dev` +- branch:`codex/awoooi-current-main-dev-base-20260624` +- base HEAD:`59485d51` +- commit / push:本段尚未 commit、尚未 push。 + +**本段完成度**: +- GitHub private backup evidence gate source / snapshot / markdown:`100%` +- FastAPI loader / read-only endpoint / 前端 API client:`100%` +- focused tests / JSON parse / doc secret / diff check:`已通過` +- 實際 GitHub private repo verification:`0/9` +- GitHub refs sync / repo creation / visibility change:`0 / false` +- 全產品 GitHub mirror ready:仍為 `0/11` + +**新增與修改範圍**: +- `scripts/security/github-target-private-backup-evidence-gate.py` +- `docs/security/github-target-private-backup-evidence-gate.snapshot.json` +- `docs/security/GITHUB-TARGET-PRIVATE-BACKUP-EVIDENCE-GATE.md` +- `docs/schemas/github_target_private_backup_evidence_gate_v1.schema.json` +- `apps/api/src/services/github_target_private_backup_evidence_gate.py` +- `apps/api/tests/test_github_target_private_backup_evidence_gate.py` +- `apps/api/tests/test_github_target_private_backup_evidence_gate_api.py` +- `apps/api/src/api/v1/agents.py` +- `apps/web/src/lib/api-client.ts` + +**目前事實**: +- approval-required GitHub targets:`9` +- unauthenticated probe publicly visible:`4`,分別是 `owenhytsai/awoooi`、`owenhytsai/clawbot-v5`、`owenhytsai/wooo-aiops`、`owenhytsai/wooo-infra-config`;全部維持 `blocked_public_probe_visible_private_evidence_required`,不得標綠。 +- `not_found_or_private`:`5`,分別是 `owenhytsai/ewoooc`、`owenhytsai/bitan-pharmacy`、`owenhytsai/tsenyang-website`、`owenhytsai/VibeWork`、`owenhytsai/agent-bounty-protocol`;這只代表未授權公開 probe 看不到,不得視為已 private,也不得視為 repo 不存在。 +- private backup verified:`0` +- private visibility owner evidence:`0/9` +- safe credential metadata accepted:`0/9` +- owner response received / accepted:`0 / 0` +- execution ready:`0` + +**驗證**: +- `python3 scripts/security/github-target-private-backup-evidence-gate.py --root .` → `GITHUB_TARGET_PRIVATE_BACKUP_EVIDENCE_GATE_BLOCKED targets=9 public_visible=4 private_verified=0 credential=0/9 refs_sync=False` +- `python3 -m py_compile scripts/security/github-target-private-backup-evidence-gate.py apps/api/src/services/github_target_private_backup_evidence_gate.py apps/api/src/api/v1/agents.py` → OK +- `python3 -m json.tool docs/security/github-target-private-backup-evidence-gate.snapshot.json` → OK +- `python3 -m json.tool docs/schemas/github_target_private_backup_evidence_gate_v1.schema.json` → OK +- `DATABASE_URL=postgresql://test:test@localhost:5432/test REDIS_URL=redis://localhost:6379/15 SECRET_KEY=test-secret ENVIRONMENT=dev pytest apps/api/tests/test_github_target_private_backup_evidence_gate.py apps/api/tests/test_github_target_private_backup_evidence_gate_api.py -q` → `6 passed` +- `python3 scripts/ops/doc-secrets-sanity-check.py docs/security/GITHUB-TARGET-PRIVATE-BACKUP-EVIDENCE-GATE.md docs/security/github-target-private-backup-evidence-gate.snapshot.json docs/schemas/github_target_private_backup_evidence_gate_v1.schema.json` → `DOC_SECRET_SANITY_OK scanned_files=3` +- `git diff --check -- ...` → OK +- `pnpm --filter @awoooi/web typecheck` → blocked:此 worktree 未安裝 `node_modules`,`tsc: command not found`;需在依賴恢復後補跑,不能把前端 typecheck 視為已通過。 + +**安全旗標**: +- `runtime_execution_authorized=false` +- `active_scan_authorized=false` +- `credentialed_scan_authorized=false` +- `host_update_authorized=false` +- `wazuh_api_live_query_authorized=false` +- `wazuh_active_response_authorized=false` +- `repo_creation_authorized=false` +- `visibility_change_authorized=false` +- `refs_sync_authorized=false` +- `workflow_modification_authorized=false` +- `workflow_trigger_authorized=false` +- `github_primary_switch_authorized=false` +- `secret_values_collected=false` +- `backup_execution_authorized=false` +- `restore_execution_authorized=false` +- `migration_authorized=false` + +**邊界與修正**: +- 本段沒有 SSH、沒有改 110/111/112/120/121/168、沒有改 Nginx / Docker / K8s / firewall、沒有觸發 Gitea / GitHub workflow、沒有建立 repo、沒有修改 visibility、沒有同步 refs、沒有讀取或保存 secret value。 +- Wazuh / SOC / Kali runtime 仍由 IwoooS 主控線處理;本段沒有新增 Wazuh UI / API,也沒有把 GitHub mirror governance 接到 Wazuh runtime。 +- 先前曾因工具預設工作目錄短暫在 `/Users/ogt/awoooi` 誤新增同名 script,已立即移除並驗證 `WRONG_WORKTREE_CLEAN_OK`;實際有效工作僅保留在受控 worktree `/Users/ogt/codex-workspaces/awoooi-dev`。 + +**下一個 P0 gate**: +- 需要 owner 提供每個 GitHub target 的 private visibility evidence ref 與 safe credential metadata evidence ref;在 `private_backup_verified=0`、`safe_credential_accepted_evidence=0/9`、`owner_response_accepted=0` 之前,不得進入 repo creation、visibility change、refs sync、workflow trigger 或 GitHub primary switch。 + ## 2026-06-25|14:41 post-start quick check live wrapper 分級讀回 **背景**:第一版 `post-start-quick-check.sh` live run 將預期中的 `escrow_missing=5` 與 MOMO 非服務面 warning 一併算成 `DEGRADED`,容易讓重啟 SOP 看起來永遠差一點。這不符合本輪目標:服務恢復、資料新鮮、備份健康、DR escrow、Wazuh registry 必須分層判定。 diff --git a/docs/schemas/github_target_private_backup_evidence_gate_v1.schema.json b/docs/schemas/github_target_private_backup_evidence_gate_v1.schema.json new file mode 100644 index 00000000..9df6efcf --- /dev/null +++ b/docs/schemas/github_target_private_backup_evidence_gate_v1.schema.json @@ -0,0 +1,160 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "github_target_private_backup_evidence_gate_v1.schema.json", + "title": "GitHub Target Private Backup Evidence Gate", + "type": "object", + "required": [ + "schema_version", + "generated_at", + "status", + "mode", + "source_reviews", + "summary", + "targets", + "acceptance_requirements", + "rejection_rules", + "operation_boundaries", + "authorization_flags" + ], + "properties": { + "schema_version": { + "const": "github_target_private_backup_evidence_gate_v1" + }, + "generated_at": { + "type": "string" + }, + "status": { + "type": "string", + "pattern": "^blocked_" + }, + "mode": { + "const": "read_only_private_backup_evidence_gate" + }, + "source_reviews": { + "type": "object" + }, + "summary": { + "type": "object", + "required": [ + "approval_required_target_count", + "public_probe_visible_target_count", + "not_found_or_private_target_count", + "private_backup_verified_count", + "safe_credential_required_count", + "safe_credential_accepted_evidence_count", + "execution_ready_count", + "repo_creation_authorized", + "visibility_change_authorized", + "refs_sync_authorized", + "github_primary_switch_authorized", + "workflow_modification_authorized", + "workflow_trigger_authorized", + "secret_value_collection_allowed", + "private_clone_url_collection_allowed", + "not_found_or_private_as_absent_allowed", + "public_repo_allowed" + ], + "properties": { + "repo_creation_authorized": { "const": false }, + "visibility_change_authorized": { "const": false }, + "refs_sync_authorized": { "const": false }, + "github_primary_switch_authorized": { "const": false }, + "workflow_modification_authorized": { "const": false }, + "workflow_trigger_authorized": { "const": false }, + "secret_value_collection_allowed": { "const": false }, + "private_clone_url_collection_allowed": { "const": false }, + "not_found_or_private_as_absent_allowed": { "const": false }, + "public_repo_allowed": { "const": false } + } + }, + "targets": { + "type": "array", + "items": { + "type": "object", + "required": [ + "github_repo", + "approval_required", + "probe_status", + "visibility_evidence_status", + "private_backup_verified", + "owner_response_accepted", + "refs_sync_ready", + "execution_ready", + "blockers", + "repo_creation_authorized", + "visibility_change_authorized", + "refs_sync_authorized", + "github_primary_switch_authorized", + "secret_values_collected" + ], + "properties": { + "private_backup_verified": { "const": false }, + "owner_response_accepted": { "const": false }, + "refs_sync_ready": { "const": false }, + "execution_ready": { "const": false }, + "repo_creation_authorized": { "const": false }, + "visibility_change_authorized": { "const": false }, + "refs_sync_authorized": { "const": false }, + "github_primary_switch_authorized": { "const": false }, + "secret_values_collected": { "const": false }, + "blockers": { + "type": "array", + "minItems": 1 + } + } + } + }, + "acceptance_requirements": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + }, + "rejection_rules": { + "type": "array", + "minItems": 1, + "items": { "type": "string" } + }, + "operation_boundaries": { + "type": "object", + "required": [ + "read_only_api_allowed", + "github_api_write_allowed", + "gitea_api_write_allowed", + "repo_creation_allowed", + "visibility_change_allowed", + "refs_sync_allowed", + "workflow_modification_allowed", + "workflow_trigger_allowed", + "github_primary_switch_allowed", + "secret_value_collection_allowed", + "private_clone_url_collection_allowed" + ], + "properties": { + "read_only_api_allowed": { "const": true }, + "github_api_write_allowed": { "const": false }, + "gitea_api_write_allowed": { "const": false }, + "repo_creation_allowed": { "const": false }, + "visibility_change_allowed": { "const": false }, + "refs_sync_allowed": { "const": false }, + "workflow_modification_allowed": { "const": false }, + "workflow_trigger_allowed": { "const": false }, + "github_primary_switch_allowed": { "const": false }, + "secret_value_collection_allowed": { "const": false }, + "private_clone_url_collection_allowed": { "const": false } + } + }, + "authorization_flags": { + "type": "object", + "properties": { + "runtime_execution_authorized": { "const": false }, + "repo_creation_authorized": { "const": false }, + "visibility_change_authorized": { "const": false }, + "refs_sync_authorized": { "const": false }, + "workflow_modification_authorized": { "const": false }, + "workflow_trigger_authorized": { "const": false }, + "github_primary_switch_authorized": { "const": false }, + "secret_values_collected": { "const": false } + } + } + } +} diff --git a/docs/security/GITHUB-TARGET-PRIVATE-BACKUP-EVIDENCE-GATE.md b/docs/security/GITHUB-TARGET-PRIVATE-BACKUP-EVIDENCE-GATE.md new file mode 100644 index 00000000..c5d4a150 --- /dev/null +++ b/docs/security/GITHUB-TARGET-PRIVATE-BACKUP-EVIDENCE-GATE.md @@ -0,0 +1,32 @@ +# GitHub Target Private Backup Evidence Gate + +| 項目 | 值 | +|------|----| +| 狀態 | `blocked_public_visibility_and_safe_credential_evidence_required` | +| approval-required targets | `9` | +| public probe visible | `4` | +| not_found_or_private | `5` | +| private backup verified | `0` | +| safe credential evidence | `0/9` | +| execution ready | `0` | + +## Target Gate + +| GitHub target | probe | visibility evidence | private verified | blockers | +|---------------|-------|---------------------|------------------|----------| +| `owenhytsai/awoooi` | `exists` | `blocked_public_probe_visible_private_evidence_required` | `false` | `4` | +| `owenhytsai/clawbot-v5` | `exists` | `blocked_public_probe_visible_private_evidence_required` | `false` | `4` | +| `owenhytsai/wooo-aiops` | `exists` | `blocked_public_probe_visible_private_evidence_required` | `false` | `4` | +| `owenhytsai/wooo-infra-config` | `exists` | `blocked_public_probe_visible_private_evidence_required` | `false` | `4` | +| `owenhytsai/ewoooc` | `not_found_or_private` | `blocked_private_or_absent_not_verified` | `false` | `4` | +| `owenhytsai/bitan-pharmacy` | `not_found_or_private` | `blocked_private_or_absent_not_verified` | `false` | `4` | +| `owenhytsai/tsenyang-website` | `not_found_or_private` | `blocked_private_or_absent_not_verified` | `false` | `4` | +| `owenhytsai/VibeWork` | `not_found_or_private` | `blocked_private_or_absent_not_verified` | `false` | `4` | +| `owenhytsai/agent-bounty-protocol` | `not_found_or_private` | `blocked_private_or_absent_not_verified` | `false` | `4` | + +## 不可誤讀 + +- 本 gate 不是 GitHub repo creation / visibility change / refs sync 授權。 +- 公開 probe 可讀的 target 需要 private visibility owner evidence,不能標綠。 +- `not_found_or_private` 不能當成已 private,也不能當成 repo 不存在。 +- safe credential evidence 只收 metadata,不收 secret value。 diff --git a/docs/security/github-target-private-backup-evidence-gate.snapshot.json b/docs/security/github-target-private-backup-evidence-gate.snapshot.json new file mode 100644 index 00000000..06f5d3da --- /dev/null +++ b/docs/security/github-target-private-backup-evidence-gate.snapshot.json @@ -0,0 +1,533 @@ +{ + "schema_version": "github_target_private_backup_evidence_gate_v1", + "generated_at": "2026-06-26T09:43:15.948000+00:00", + "status": "blocked_public_visibility_and_safe_credential_evidence_required", + "mode": "read_only_private_backup_evidence_gate", + "source_reviews": { + "github_target_decision": "docs/security/github-target-decision.snapshot.json", + "github_target_owner_decision_response": "docs/security/github-target-owner-decision-response.snapshot.json", + "github_target_repo_approval_package": "docs/security/github-target-repo-approval-package.snapshot.json" + }, + "summary": { + "target_decision_count": 10, + "approval_required_target_count": 9, + "approval_package_item_count": 9, + "public_probe_visible_target_count": 4, + "not_found_or_private_target_count": 5, + "private_backup_verified_count": 0, + "private_visibility_evidence_missing_count": 9, + "safe_credential_required_count": 9, + "safe_credential_accepted_evidence_count": 0, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "execution_ready_count": 0, + "blocked_target_count": 9, + "external_scope_target_count": 1, + "forbidden_action_count": 12, + "repo_creation_authorized": false, + "visibility_change_authorized": false, + "refs_sync_authorized": false, + "github_primary_switch_authorized": false, + "workflow_modification_authorized": false, + "workflow_trigger_authorized": false, + "secret_value_collection_allowed": false, + "private_clone_url_collection_allowed": false, + "not_found_or_private_as_absent_allowed": false, + "public_repo_allowed": false + }, + "targets": [ + { + "github_repo": "owenhytsai/awoooi", + "source_key": "wooo/awoooi", + "approval_required": true, + "probe_status": "exists", + "target_state": "exists_refs_blocked", + "risk": "HIGH", + "visibility_evidence_status": "blocked_public_probe_visible_private_evidence_required", + "private_backup_verified": false, + "private_visibility_owner_evidence_ref": null, + "safe_credential_evidence_status": "missing_safe_credential_metadata", + "safe_credential_evidence_ref": null, + "owner_response_accepted": false, + "refs_sync_ready": false, + "execution_ready": false, + "blockers": [ + "github_target_publicly_readable_by_unauthenticated_probe", + "private_visibility_owner_evidence_missing", + "safe_credential_metadata_missing", + "refs_sync_not_authorized" + ], + "evidence_refs": [ + "docs/security/GITEA-GITHUB-MIGRATION-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json", + "docs/security/github-target-owner-decision-response.snapshot.json" + ], + "forbidden_actions": [ + "create_github_repo", + "change_repo_visibility", + "push_refs", + "delete_refs", + "force_push", + "mirror_sync", + "switch_github_primary", + "disable_gitea", + "workflow_modification", + "workflow_trigger", + "secret_value_collection", + "private_clone_url_collection" + ], + "repo_creation_authorized": false, + "visibility_change_authorized": false, + "refs_sync_authorized": false, + "github_primary_switch_authorized": false, + "secret_values_collected": false + }, + { + "github_repo": "owenhytsai/clawbot-v5", + "source_key": "wooo/clawbot-v5", + "approval_required": true, + "probe_status": "exists", + "target_state": "exists_refs_blocked", + "risk": "MEDIUM", + "visibility_evidence_status": "blocked_public_probe_visible_private_evidence_required", + "private_backup_verified": false, + "private_visibility_owner_evidence_ref": null, + "safe_credential_evidence_status": "missing_safe_credential_metadata", + "safe_credential_evidence_ref": null, + "owner_response_accepted": false, + "refs_sync_ready": false, + "execution_ready": false, + "blockers": [ + "github_target_publicly_readable_by_unauthenticated_probe", + "private_visibility_owner_evidence_missing", + "safe_credential_metadata_missing", + "refs_sync_not_authorized" + ], + "evidence_refs": [ + "docs/security/SOURCE-CONTROL-CLAWBOT-V5-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json", + "docs/security/github-target-owner-decision-response.snapshot.json" + ], + "forbidden_actions": [ + "create_github_repo", + "change_repo_visibility", + "push_refs", + "delete_refs", + "force_push", + "mirror_sync", + "switch_github_primary", + "disable_gitea", + "workflow_modification", + "workflow_trigger", + "secret_value_collection", + "private_clone_url_collection" + ], + "repo_creation_authorized": false, + "visibility_change_authorized": false, + "refs_sync_authorized": false, + "github_primary_switch_authorized": false, + "secret_values_collected": false + }, + { + "github_repo": "owenhytsai/wooo-aiops", + "source_key": "wooo/wooo-aiops", + "approval_required": true, + "probe_status": "exists", + "target_state": "exists_refs_blocked", + "risk": "MEDIUM", + "visibility_evidence_status": "blocked_public_probe_visible_private_evidence_required", + "private_backup_verified": false, + "private_visibility_owner_evidence_ref": null, + "safe_credential_evidence_status": "missing_safe_credential_metadata", + "safe_credential_evidence_ref": null, + "owner_response_accepted": false, + "refs_sync_ready": false, + "execution_ready": false, + "blockers": [ + "github_target_publicly_readable_by_unauthenticated_probe", + "private_visibility_owner_evidence_missing", + "safe_credential_metadata_missing", + "refs_sync_not_authorized" + ], + "evidence_refs": [ + "docs/security/SOURCE-CONTROL-WOOO-AIOPS-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json", + "docs/security/github-target-owner-decision-response.snapshot.json" + ], + "forbidden_actions": [ + "create_github_repo", + "change_repo_visibility", + "push_refs", + "delete_refs", + "force_push", + "mirror_sync", + "switch_github_primary", + "disable_gitea", + "workflow_modification", + "workflow_trigger", + "secret_value_collection", + "private_clone_url_collection" + ], + "repo_creation_authorized": false, + "visibility_change_authorized": false, + "refs_sync_authorized": false, + "github_primary_switch_authorized": false, + "secret_values_collected": false + }, + { + "github_repo": "owenhytsai/wooo-infra-config", + "source_key": "wooo/wooo-infra-config", + "approval_required": true, + "probe_status": "exists", + "target_state": "exists_aligned", + "risk": "MEDIUM", + "visibility_evidence_status": "blocked_public_probe_visible_private_evidence_required", + "private_backup_verified": false, + "private_visibility_owner_evidence_ref": null, + "safe_credential_evidence_status": "missing_safe_credential_metadata", + "safe_credential_evidence_ref": null, + "owner_response_accepted": false, + "refs_sync_ready": false, + "execution_ready": false, + "blockers": [ + "github_target_publicly_readable_by_unauthenticated_probe", + "private_visibility_owner_evidence_missing", + "safe_credential_metadata_missing", + "refs_sync_not_authorized" + ], + "evidence_refs": [ + "docs/security/GIT-REMOTE-REFS-WOOO-INFRA-CONFIG-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json", + "docs/security/github-target-owner-decision-response.snapshot.json" + ], + "forbidden_actions": [ + "create_github_repo", + "change_repo_visibility", + "push_refs", + "delete_refs", + "force_push", + "mirror_sync", + "switch_github_primary", + "disable_gitea", + "workflow_modification", + "workflow_trigger", + "secret_value_collection", + "private_clone_url_collection" + ], + "repo_creation_authorized": false, + "visibility_change_authorized": false, + "refs_sync_authorized": false, + "github_primary_switch_authorized": false, + "secret_values_collected": false + }, + { + "github_repo": "owenhytsai/ewoooc", + "source_key": "wooo/ewoooc / root/momo-pro-system / momo working trees", + "approval_required": true, + "probe_status": "not_found_or_private", + "target_state": "not_found_or_private", + "risk": "HIGH", + "visibility_evidence_status": "blocked_private_or_absent_not_verified", + "private_backup_verified": false, + "private_visibility_owner_evidence_ref": null, + "safe_credential_evidence_status": "missing_safe_credential_metadata", + "safe_credential_evidence_ref": null, + "owner_response_accepted": false, + "refs_sync_ready": false, + "execution_ready": false, + "blockers": [ + "not_found_or_private_is_not_private_verification", + "private_visibility_owner_evidence_missing", + "safe_credential_metadata_missing", + "refs_sync_not_authorized" + ], + "evidence_refs": [ + "docs/security/GITEA-PUBLIC-REPO-SEARCH-SNAPSHOT.md", + "docs/security/LOCAL-REPO-CANONICAL-EWOOOC-MOMO-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json", + "docs/security/github-target-owner-decision-response.snapshot.json" + ], + "forbidden_actions": [ + "create_github_repo", + "change_repo_visibility", + "push_refs", + "delete_refs", + "force_push", + "mirror_sync", + "switch_github_primary", + "disable_gitea", + "workflow_modification", + "workflow_trigger", + "secret_value_collection", + "private_clone_url_collection" + ], + "repo_creation_authorized": false, + "visibility_change_authorized": false, + "refs_sync_authorized": false, + "github_primary_switch_authorized": false, + "secret_values_collected": false + }, + { + "github_repo": "owenhytsai/bitan-pharmacy", + "source_key": "bitan-pharmacy", + "approval_required": true, + "probe_status": "not_found_or_private", + "target_state": "not_found_or_private", + "risk": "MEDIUM", + "visibility_evidence_status": "blocked_private_or_absent_not_verified", + "private_backup_verified": false, + "private_visibility_owner_evidence_ref": null, + "safe_credential_evidence_status": "missing_safe_credential_metadata", + "safe_credential_evidence_ref": null, + "owner_response_accepted": false, + "refs_sync_ready": false, + "execution_ready": false, + "blockers": [ + "not_found_or_private_is_not_private_verification", + "private_visibility_owner_evidence_missing", + "safe_credential_metadata_missing", + "refs_sync_not_authorized" + ], + "evidence_refs": [ + "docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json", + "docs/security/github-target-owner-decision-response.snapshot.json" + ], + "forbidden_actions": [ + "create_github_repo", + "change_repo_visibility", + "push_refs", + "delete_refs", + "force_push", + "mirror_sync", + "switch_github_primary", + "disable_gitea", + "workflow_modification", + "workflow_trigger", + "secret_value_collection", + "private_clone_url_collection" + ], + "repo_creation_authorized": false, + "visibility_change_authorized": false, + "refs_sync_authorized": false, + "github_primary_switch_authorized": false, + "secret_values_collected": false + }, + { + "github_repo": "owenhytsai/tsenyang-website", + "source_key": "tsenyang-website", + "approval_required": true, + "probe_status": "not_found_or_private", + "target_state": "not_found_or_private", + "risk": "MEDIUM", + "visibility_evidence_status": "blocked_private_or_absent_not_verified", + "private_backup_verified": false, + "private_visibility_owner_evidence_ref": null, + "safe_credential_evidence_status": "missing_safe_credential_metadata", + "safe_credential_evidence_ref": null, + "owner_response_accepted": false, + "refs_sync_ready": false, + "execution_ready": false, + "blockers": [ + "not_found_or_private_is_not_private_verification", + "private_visibility_owner_evidence_missing", + "safe_credential_metadata_missing", + "refs_sync_not_authorized" + ], + "evidence_refs": [ + "docs/security/GIT-REMOTE-REFS-BITAN-TSENYANG-SNAPSHOT.md", + "docs/security/github-target-probe.snapshot.json", + "docs/security/github-target-owner-decision-response.snapshot.json" + ], + "forbidden_actions": [ + "create_github_repo", + "change_repo_visibility", + "push_refs", + "delete_refs", + "force_push", + "mirror_sync", + "switch_github_primary", + "disable_gitea", + "workflow_modification", + "workflow_trigger", + "secret_value_collection", + "private_clone_url_collection" + ], + "repo_creation_authorized": false, + "visibility_change_authorized": false, + "refs_sync_authorized": false, + "github_primary_switch_authorized": false, + "secret_values_collected": false + }, + { + "github_repo": "nexu-io/open-design", + "source_key": "open-design", + "approval_required": false, + "probe_status": "exists", + "target_state": "external_scope", + "risk": "LOW", + "visibility_evidence_status": "external_scope_not_backup_target", + "private_backup_verified": false, + "private_visibility_owner_evidence_ref": null, + "safe_credential_evidence_status": "not_required_external_scope", + "safe_credential_evidence_ref": null, + "owner_response_accepted": false, + "refs_sync_ready": false, + "execution_ready": false, + "blockers": [ + "external_scope_review_only" + ], + "evidence_refs": [ + "docs/security/github-target-probe.snapshot.json" + ], + "forbidden_actions": [ + "create_github_repo", + "change_repo_visibility", + "push_refs", + "delete_refs", + "force_push", + "mirror_sync", + "switch_github_primary", + "disable_gitea", + "workflow_modification", + "workflow_trigger", + "secret_value_collection", + "private_clone_url_collection" + ], + "repo_creation_authorized": false, + "visibility_change_authorized": false, + "refs_sync_authorized": false, + "github_primary_switch_authorized": false, + "secret_values_collected": false + }, + { + "github_repo": "owenhytsai/VibeWork", + "source_key": "vibework", + "approval_required": true, + "probe_status": "not_found_or_private", + "target_state": "not_found_or_private", + "risk": "HIGH", + "visibility_evidence_status": "blocked_private_or_absent_not_verified", + "private_backup_verified": false, + "private_visibility_owner_evidence_ref": null, + "safe_credential_evidence_status": "missing_safe_credential_metadata", + "safe_credential_evidence_ref": null, + "owner_response_accepted": false, + "refs_sync_ready": false, + "execution_ready": false, + "blockers": [ + "not_found_or_private_is_not_private_verification", + "private_visibility_owner_evidence_missing", + "safe_credential_metadata_missing", + "refs_sync_not_authorized" + ], + "evidence_refs": [ + "docs/security/source-control-workflow-secret-name-local-evidence.snapshot.json", + "docs/security/source-control-primary-readiness-gate.snapshot.json", + "docs/security/github-target-owner-decision-response.snapshot.json" + ], + "forbidden_actions": [ + "create_github_repo", + "change_repo_visibility", + "push_refs", + "delete_refs", + "force_push", + "mirror_sync", + "switch_github_primary", + "disable_gitea", + "workflow_modification", + "workflow_trigger", + "secret_value_collection", + "private_clone_url_collection" + ], + "repo_creation_authorized": false, + "visibility_change_authorized": false, + "refs_sync_authorized": false, + "github_primary_switch_authorized": false, + "secret_values_collected": false + }, + { + "github_repo": "owenhytsai/agent-bounty-protocol", + "source_key": "agent-bounty-protocol", + "approval_required": true, + "probe_status": "not_found_or_private", + "target_state": "not_found_or_private", + "risk": "HIGH", + "visibility_evidence_status": "blocked_private_or_absent_not_verified", + "private_backup_verified": false, + "private_visibility_owner_evidence_ref": null, + "safe_credential_evidence_status": "missing_safe_credential_metadata", + "safe_credential_evidence_ref": null, + "owner_response_accepted": false, + "refs_sync_ready": false, + "execution_ready": false, + "blockers": [ + "not_found_or_private_is_not_private_verification", + "private_visibility_owner_evidence_missing", + "safe_credential_metadata_missing", + "refs_sync_not_authorized" + ], + "evidence_refs": [ + "docs/security/source-control-workflow-secret-name-local-evidence.snapshot.json", + "docs/security/source-control-primary-readiness-gate.snapshot.json", + "docs/security/github-target-owner-decision-response.snapshot.json" + ], + "forbidden_actions": [ + "create_github_repo", + "change_repo_visibility", + "push_refs", + "delete_refs", + "force_push", + "mirror_sync", + "switch_github_primary", + "disable_gitea", + "workflow_modification", + "workflow_trigger", + "secret_value_collection", + "private_clone_url_collection" + ], + "repo_creation_authorized": false, + "visibility_change_authorized": false, + "refs_sync_authorized": false, + "github_primary_switch_authorized": false, + "secret_values_collected": false + } + ], + "acceptance_requirements": [ + "每個 approval-required GitHub target 必須有 private visibility owner evidence ref。", + "公開 probe 可讀的 target 不得被視為符合私有備援要求。", + "`not_found_or_private` 只代表未授權只讀 probe 看不到,不得當成 private verified 或 repo absent。", + "safe credential evidence 只允許 credential storage / owner / scope / rotation metadata,不得收 token value。", + "owner response accepted count 在 reviewer acceptance 前必須維持 0。", + "private evidence 與 safe credential evidence 完整前不得建立 repo、改 visibility、push refs 或切 GitHub primary。" + ], + "rejection_rules": [ + "任何 public repo 或 unauthenticated readable target 均不得標示 private_backup_verified=true。", + "任何 token、PAT、private key、cookie、session、private clone credential 或 partial secret 必須拒收。", + "任何 repo creation、visibility change、refs sync、force push、tag rewrite、workflow trigger 或 primary switch request 必須拒收。", + "任何把 `not_found_or_private` 解讀為 repo 不存在或可建立新 repo 的 response 必須拒收。" + ], + "operation_boundaries": { + "read_only_api_allowed": true, + "github_api_write_allowed": false, + "gitea_api_write_allowed": false, + "repo_creation_allowed": false, + "visibility_change_allowed": false, + "refs_sync_allowed": false, + "workflow_modification_allowed": false, + "workflow_trigger_allowed": false, + "github_primary_switch_allowed": false, + "secret_value_collection_allowed": false, + "private_clone_url_collection_allowed": false + }, + "authorization_flags": { + "runtime_execution_authorized": false, + "repo_creation_authorized": false, + "visibility_change_authorized": false, + "refs_sync_authorized": false, + "workflow_modification_authorized": false, + "workflow_trigger_authorized": false, + "github_primary_switch_authorized": false, + "secret_values_collected": false + } +} diff --git a/scripts/security/github-target-private-backup-evidence-gate.py b/scripts/security/github-target-private-backup-evidence-gate.py new file mode 100644 index 00000000..e99bdf0c --- /dev/null +++ b/scripts/security/github-target-private-backup-evidence-gate.py @@ -0,0 +1,298 @@ +#!/usr/bin/env python3 +"""GitHub 私有備援 evidence gate。 + +此工具只讀既有 GitHub target decision / owner response / approval package +snapshot,產生「私有備援是否可進入執行」的 fail-closed gate。它不呼叫 +GitHub / Gitea API、不建立 repo、不修改 visibility、不同步 refs、不讀取 +或保存任何 secret value。 +""" + +from __future__ import annotations + +import argparse +import json +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +SCHEMA_VERSION = "github_target_private_backup_evidence_gate_v1" +FORBIDDEN_ACTIONS = [ + "create_github_repo", + "change_repo_visibility", + "push_refs", + "delete_refs", + "force_push", + "mirror_sync", + "switch_github_primary", + "disable_gitea", + "workflow_modification", + "workflow_trigger", + "secret_value_collection", + "private_clone_url_collection", +] + + +def load_json(path: Path) -> dict[str, Any]: + with path.open(encoding="utf-8") as handle: + payload = json.load(handle) + if not isinstance(payload, dict): + raise ValueError(f"{path}: expected JSON object") + return payload + + +def build_target_gate(decision: dict[str, Any]) -> dict[str, Any]: + approval_required = bool(decision.get("approval_required")) + probe_status = str(decision.get("probe_status") or "unknown") + github_repo = str(decision.get("github_repo") or "") + + if not approval_required: + visibility_status = "external_scope_not_backup_target" + blockers = ["external_scope_review_only"] + elif probe_status.startswith("exists"): + visibility_status = "blocked_public_probe_visible_private_evidence_required" + blockers = [ + "github_target_publicly_readable_by_unauthenticated_probe", + "private_visibility_owner_evidence_missing", + "safe_credential_metadata_missing", + "refs_sync_not_authorized", + ] + elif probe_status == "not_found_or_private": + visibility_status = "blocked_private_or_absent_not_verified" + blockers = [ + "not_found_or_private_is_not_private_verification", + "private_visibility_owner_evidence_missing", + "safe_credential_metadata_missing", + "refs_sync_not_authorized", + ] + else: + visibility_status = "blocked_probe_status_unknown" + blockers = [ + "github_target_probe_status_unknown", + "private_visibility_owner_evidence_missing", + "safe_credential_metadata_missing", + "refs_sync_not_authorized", + ] + + return { + "github_repo": github_repo, + "source_key": decision.get("source_key"), + "approval_required": approval_required, + "probe_status": probe_status, + "target_state": decision.get("target_state"), + "risk": decision.get("risk"), + "visibility_evidence_status": visibility_status, + "private_backup_verified": False, + "private_visibility_owner_evidence_ref": None, + "safe_credential_evidence_status": ( + "not_required_external_scope" if not approval_required else "missing_safe_credential_metadata" + ), + "safe_credential_evidence_ref": None, + "owner_response_accepted": False, + "refs_sync_ready": False, + "execution_ready": False, + "blockers": blockers, + "evidence_refs": decision.get("evidence_refs", []), + "forbidden_actions": FORBIDDEN_ACTIONS, + "repo_creation_authorized": False, + "visibility_change_authorized": False, + "refs_sync_authorized": False, + "github_primary_switch_authorized": False, + "secret_values_collected": False, + } + + +def count_targets(targets: list[dict[str, Any]], predicate) -> int: + return sum(1 for target in targets if predicate(target)) + + +def build_payload( + decision_snapshot: dict[str, Any], + owner_response_snapshot: dict[str, Any], + approval_package_snapshot: dict[str, Any], +) -> dict[str, Any]: + decisions = decision_snapshot.get("decisions") or [] + if not isinstance(decisions, list): + raise ValueError("github target decision snapshot missing decisions") + + targets = [build_target_gate(decision) for decision in decisions if isinstance(decision, dict)] + approval_targets = [target for target in targets if target["approval_required"]] + public_visible_targets = [ + target for target in approval_targets if str(target["probe_status"]).startswith("exists") + ] + unknown_private_targets = [ + target for target in approval_targets if target["probe_status"] == "not_found_or_private" + ] + + owner_summary = owner_response_snapshot.get("summary") or {} + package_items = approval_package_snapshot.get("approval_items") or [] + received_count = int(owner_summary.get("received_response_count", 0) or 0) + accepted_count = int(owner_summary.get("accepted_response_count", 0) or 0) + + status = "blocked_public_visibility_and_safe_credential_evidence_required" + if not public_visible_targets: + status = "blocked_private_visibility_and_safe_credential_evidence_required" + + summary = { + "target_decision_count": len(targets), + "approval_required_target_count": len(approval_targets), + "approval_package_item_count": len(package_items), + "public_probe_visible_target_count": len(public_visible_targets), + "not_found_or_private_target_count": len(unknown_private_targets), + "private_backup_verified_count": 0, + "private_visibility_evidence_missing_count": len(approval_targets), + "safe_credential_required_count": len(approval_targets), + "safe_credential_accepted_evidence_count": 0, + "owner_response_received_count": received_count, + "owner_response_accepted_count": accepted_count, + "execution_ready_count": 0, + "blocked_target_count": len(approval_targets), + "external_scope_target_count": count_targets(targets, lambda target: not target["approval_required"]), + "forbidden_action_count": len(FORBIDDEN_ACTIONS), + "repo_creation_authorized": False, + "visibility_change_authorized": False, + "refs_sync_authorized": False, + "github_primary_switch_authorized": False, + "workflow_modification_authorized": False, + "workflow_trigger_authorized": False, + "secret_value_collection_allowed": False, + "private_clone_url_collection_allowed": False, + "not_found_or_private_as_absent_allowed": False, + "public_repo_allowed": False, + } + + return { + "schema_version": SCHEMA_VERSION, + "generated_at": datetime.now(timezone.utc).isoformat(), + "status": status, + "mode": "read_only_private_backup_evidence_gate", + "source_reviews": { + "github_target_decision": "docs/security/github-target-decision.snapshot.json", + "github_target_owner_decision_response": "docs/security/github-target-owner-decision-response.snapshot.json", + "github_target_repo_approval_package": "docs/security/github-target-repo-approval-package.snapshot.json", + }, + "summary": summary, + "targets": targets, + "acceptance_requirements": [ + "每個 approval-required GitHub target 必須有 private visibility owner evidence ref。", + "公開 probe 可讀的 target 不得被視為符合私有備援要求。", + "`not_found_or_private` 只代表未授權只讀 probe 看不到,不得當成 private verified 或 repo absent。", + "safe credential evidence 只允許 credential storage / owner / scope / rotation metadata,不得收 token value。", + "owner response accepted count 在 reviewer acceptance 前必須維持 0。", + "private evidence 與 safe credential evidence 完整前不得建立 repo、改 visibility、push refs 或切 GitHub primary。", + ], + "rejection_rules": [ + "任何 public repo 或 unauthenticated readable target 均不得標示 private_backup_verified=true。", + "任何 token、PAT、private key、cookie、session、private clone credential 或 partial secret 必須拒收。", + "任何 repo creation、visibility change、refs sync、force push、tag rewrite、workflow trigger 或 primary switch request 必須拒收。", + "任何把 `not_found_or_private` 解讀為 repo 不存在或可建立新 repo 的 response 必須拒收。", + ], + "operation_boundaries": { + "read_only_api_allowed": True, + "github_api_write_allowed": False, + "gitea_api_write_allowed": False, + "repo_creation_allowed": False, + "visibility_change_allowed": False, + "refs_sync_allowed": False, + "workflow_modification_allowed": False, + "workflow_trigger_allowed": False, + "github_primary_switch_allowed": False, + "secret_value_collection_allowed": False, + "private_clone_url_collection_allowed": False, + }, + "authorization_flags": { + "runtime_execution_authorized": False, + "repo_creation_authorized": False, + "visibility_change_authorized": False, + "refs_sync_authorized": False, + "workflow_modification_authorized": False, + "workflow_trigger_authorized": False, + "github_primary_switch_authorized": False, + "secret_values_collected": False, + }, + } + + +def write_markdown(payload: dict[str, Any], path: Path) -> None: + summary = payload["summary"] + lines = [ + "# GitHub Target Private Backup Evidence Gate", + "", + "| 項目 | 值 |", + "|------|----|", + f"| 狀態 | `{payload['status']}` |", + f"| approval-required targets | `{summary['approval_required_target_count']}` |", + f"| public probe visible | `{summary['public_probe_visible_target_count']}` |", + f"| not_found_or_private | `{summary['not_found_or_private_target_count']}` |", + f"| private backup verified | `{summary['private_backup_verified_count']}` |", + f"| safe credential evidence | `{summary['safe_credential_accepted_evidence_count']}/{summary['safe_credential_required_count']}` |", + f"| execution ready | `{summary['execution_ready_count']}` |", + "", + "## Target Gate", + "", + "| GitHub target | probe | visibility evidence | private verified | blockers |", + "|---------------|-------|---------------------|------------------|----------|", + ] + for target in payload["targets"]: + if not target["approval_required"]: + continue + lines.append( + "| " + + " | ".join( + [ + f"`{target['github_repo']}`", + f"`{target['probe_status']}`", + f"`{target['visibility_evidence_status']}`", + f"`{str(target['private_backup_verified']).lower()}`", + f"`{len(target['blockers'])}`", + ] + ) + + " |" + ) + lines.extend( + [ + "", + "## 不可誤讀", + "", + "- 本 gate 不是 GitHub repo creation / visibility change / refs sync 授權。", + "- 公開 probe 可讀的 target 需要 private visibility owner evidence,不能標綠。", + "- `not_found_or_private` 不能當成已 private,也不能當成 repo 不存在。", + "- safe credential evidence 只收 metadata,不收 secret value。", + "", + ] + ) + path.write_text("\n".join(lines), encoding="utf-8") + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--root", default=".") + parser.add_argument("--output-json", default="docs/security/github-target-private-backup-evidence-gate.snapshot.json") + parser.add_argument("--output-md", default="docs/security/GITHUB-TARGET-PRIVATE-BACKUP-EVIDENCE-GATE.md") + args = parser.parse_args() + + root = Path(args.root) + payload = build_payload( + load_json(root / "docs/security/github-target-decision.snapshot.json"), + load_json(root / "docs/security/github-target-owner-decision-response.snapshot.json"), + load_json(root / "docs/security/github-target-repo-approval-package.snapshot.json"), + ) + output_json = root / args.output_json + output_md = root / args.output_md + output_json.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + write_markdown(payload, output_md) + + summary = payload["summary"] + print( + "GITHUB_TARGET_PRIVATE_BACKUP_EVIDENCE_GATE_BLOCKED " + f"targets={summary['approval_required_target_count']} " + f"public_visible={summary['public_probe_visible_target_count']} " + f"private_verified={summary['private_backup_verified_count']} " + f"credential={summary['safe_credential_accepted_evidence_count']}/{summary['safe_credential_required_count']} " + f"refs_sync={summary['refs_sync_authorized']}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())