feat(security): add GitHub private backup evidence gate
This commit is contained in:
@@ -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],
|
||||
|
||||
@@ -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}]")
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -704,6 +704,11 @@ export const apiClient = {
|
||||
return handleResponse<GiteaWorkflowRunnerHealthSnapshot>(res)
|
||||
},
|
||||
|
||||
async getGithubTargetPrivateBackupEvidenceGate() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/github-target-private-backup-evidence-gate`)
|
||||
return handleResponse<GithubTargetPrivateBackupEvidenceGateSnapshot>(res)
|
||||
},
|
||||
|
||||
async getObservabilityContractMatrix() {
|
||||
const res = await fetch(`${API_BASE_URL}/agents/observability-contract-matrix`)
|
||||
return handleResponse<ObservabilityContractMatrixSnapshot>(res)
|
||||
@@ -12587,6 +12592,87 @@ export interface GiteaWorkflowRunnerHealthSnapshot {
|
||||
approval_boundaries: Record<string, false>
|
||||
}
|
||||
|
||||
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<string, string>
|
||||
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<string, false>
|
||||
}
|
||||
|
||||
export interface ObservabilityContractMatrixSnapshot {
|
||||
schema_version: 'observability_contract_matrix_v1'
|
||||
generated_at: string
|
||||
|
||||
@@ -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 必須分層判定。
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
32
docs/security/GITHUB-TARGET-PRIVATE-BACKUP-EVIDENCE-GATE.md
Normal file
32
docs/security/GITHUB-TARGET-PRIVATE-BACKUP-EVIDENCE-GATE.md
Normal file
@@ -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。
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
298
scripts/security/github-target-private-backup-evidence-gate.py
Normal file
298
scripts/security/github-target-private-backup-evidence-gate.py
Normal file
@@ -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())
|
||||
Reference in New Issue
Block a user