398 lines
19 KiB
Python
398 lines
19 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
S4.9 owner response gate 現況缺口稽核 snapshot 產生器。
|
||
|
||
本工具只讀 committed snapshot、文件與 git metadata,整理哪些要求仍未符合、
|
||
哪些規範需新增、哪些規範需調整。它不送 owner request、不收 owner response、
|
||
不連 Gitea / GitHub、不讀 secret、不做 runtime action。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import subprocess
|
||
import sys
|
||
from datetime import datetime, timedelta, timezone
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
|
||
TAIPEI = timezone(timedelta(hours=8))
|
||
|
||
FALSE_BOUNDARIES = {
|
||
"request_dispatch_authorized": False,
|
||
"request_sent": False,
|
||
"owner_response_received": False,
|
||
"owner_response_accepted": False,
|
||
"redacted_payload_ingested": False,
|
||
"repo_creation_authorized": False,
|
||
"visibility_change_authorized": False,
|
||
"refs_sync_authorized": False,
|
||
"force_push_authorized": False,
|
||
"workflow_modification_authorized": False,
|
||
"runner_enablement_authorized": False,
|
||
"secret_value_collection_allowed": False,
|
||
"raw_namespace_public_surface_allowed": False,
|
||
"work_session_transcript_public_allowed": False,
|
||
"runtime_execution_authorized": False,
|
||
"action_buttons_allowed": False,
|
||
}
|
||
|
||
CURRENT_REQUIREMENT_GAPS = [
|
||
{
|
||
"gap_id": "s49_owner_response_absent",
|
||
"priority": "P0",
|
||
"status": "active_blocker",
|
||
"requirement": "S4.9 必須收到完整 owner response metadata,且五題都通過 reviewer checklist。",
|
||
"current_state": "request_sent=false、received=0、accepted=0、rejected=0。",
|
||
"required_next_step": "只能等待人工送件與 owner 脫敏回覆;不得用 UI、LOGBOOK 或 AwoooP approval 補成 accepted。",
|
||
},
|
||
{
|
||
"gap_id": "s49_dispatch_audit_event_absent",
|
||
"priority": "P0",
|
||
"status": "active_blocker",
|
||
"requirement": "request shown、request sent、owner response received、reviewer outcome 必須是分離 audit metadata。",
|
||
"current_state": "audit event templates 存在,但 emitted_event_count=0。",
|
||
"required_next_step": "送件前維持 0;送件後只保存 metadata,不保存 raw response body。",
|
||
},
|
||
{
|
||
"gap_id": "s49_reviewer_outcome_absent",
|
||
"priority": "P0",
|
||
"status": "active_blocker",
|
||
"requirement": "任何 owner response 都要先分類為 waiting、supplement、quarantine、reject 或 read-only update candidate。",
|
||
"current_state": "尚無 owner response,reviewer outcome 仍是 template-only。",
|
||
"required_next_step": "補 reviewer outcome ledger,但不得因此開 runtime gate。",
|
||
},
|
||
{
|
||
"gap_id": "public_surface_identity_leak_risk",
|
||
"priority": "P0",
|
||
"status": "mitigated_needs_guard",
|
||
"requirement": "前台與瀏覽器 API 不得顯示個人 namespace、外部 org namespace、工作視窗對話或內部 session 語句。",
|
||
"current_state": "AwoooP 高可見頁已改用公開名稱 / SRC-###;仍需由 guard 固定 public surface redaction 規則。",
|
||
"required_next_step": "持續跑 security mirror guard;新增頁面、API payload 或 client bundle 時一律先做 sensitive public-surface scan。",
|
||
},
|
||
{
|
||
"gap_id": "raw_namespace_internal_evidence_misroute_risk",
|
||
"priority": "P0",
|
||
"status": "active_risk",
|
||
"requirement": "內部 source-control evidence 可保留必要技術識別,但不得被路由到產品頁、public API、公開 bundle 或截圖文案。",
|
||
"current_state": "部分 docs/security source-control evidence 仍有 raw repo identifiers,屬內部 evidence;需要明確 public/private boundary。",
|
||
"required_next_step": "所有前台資料源必須使用 redacted scope id 或 evidence ref,不直接讀 raw source-control evidence。",
|
||
},
|
||
{
|
||
"gap_id": "latest_basis_staleness_risk",
|
||
"priority": "P0",
|
||
"status": "remediated_by_this_snapshot",
|
||
"requirement": "S4.9 缺口稽核基準需跟上最新 gitea/main、deploy marker、production verification 與 LOGBOOK。",
|
||
"current_state": "舊 MD / 產生器曾停在 2026-06-14 以前的 deploy marker;本 snapshot 將基準更新到最新 AwoooP 高可見頁脫敏正式驗證。",
|
||
"required_next_step": "每次推送前 fetch gitea 並更新 basis,不得沿用舊 commit 宣稱最新。",
|
||
},
|
||
{
|
||
"gap_id": "parallel_session_conflict_risk",
|
||
"priority": "P0",
|
||
"status": "active_risk",
|
||
"requirement": "另一個 AwoooP Session 與本 Session 的 commit、run、deploy marker、production evidence 必須同步。",
|
||
"current_state": "本輪已 fetch 且 worktree 與 gitea/main 一致;仍需每次推送前重查。",
|
||
"required_next_step": "禁止 force push;若 gitea/main 前進,只能 fast-forward / rebase 正常收斂。",
|
||
},
|
||
{
|
||
"gap_id": "release_worktree_memory_index_absent",
|
||
"priority": "P1",
|
||
"status": "active_process_gap",
|
||
"requirement": "Session 啟動必讀 MEMORY.md;若 release worktree 缺檔,必須改讀全域 memory 與 LOGBOOK,並記錄例外。",
|
||
"current_state": "此 release worktree 未包含 repo-local MEMORY.md。",
|
||
"required_next_step": "把缺檔視為啟動程序缺口,不阻擋只讀工作,但不得聲稱已讀 repo-local MEMORY.md。",
|
||
},
|
||
]
|
||
|
||
NEW_RULES_REQUIRED = [
|
||
{
|
||
"rule_id": "public_surface_redaction_gate",
|
||
"priority": "P0",
|
||
"rule": "前台、public API、HTML、bundle、messages 不得出現 raw owner namespace、個人識別或工作視窗對話。",
|
||
"verification": "security-mirror-progress-guard + production desktop/mobile sensitive scan。",
|
||
},
|
||
{
|
||
"rule_id": "s49_gap_audit_snapshot_required",
|
||
"priority": "P0",
|
||
"rule": "S4.9 現況缺口稽核不得只停在 MD,必須有 machine-readable snapshot 與 guard。",
|
||
"verification": "source-control-owner-response-guard 必須讀取本 snapshot。",
|
||
},
|
||
{
|
||
"rule_id": "raw_evidence_private_boundary",
|
||
"priority": "P0",
|
||
"rule": "內部 source-control raw evidence 僅能留在 private docs / committed evidence,不得直接成為產品渲染來源。",
|
||
"verification": "前台只讀 redacted scope id、evidence ref 或 aggregate count。",
|
||
},
|
||
{
|
||
"rule_id": "owner_response_counts_are_runtime_locked",
|
||
"priority": "P0",
|
||
"rule": "owner response received / accepted / rejected 只能由 reviewer record 更新,不得由 request draft、UI 或 LOGBOOK 文字更新。",
|
||
"verification": "所有 snapshot false boundaries 維持 0 / false。",
|
||
},
|
||
{
|
||
"rule_id": "dispatch_received_accepted_separation",
|
||
"priority": "P0",
|
||
"rule": "request ready、request sent、response received、response accepted、runtime approval 必須是五個獨立狀態。",
|
||
"verification": "summary counters 必須分離,且 runtime gate 永遠等待獨立批准。",
|
||
},
|
||
{
|
||
"rule_id": "parallel_session_basis_refresh",
|
||
"priority": "P0",
|
||
"rule": "每次 commit / push 前必須 fetch gitea,並同步另一 Session 的 run / deploy / production evidence。",
|
||
"verification": "LOGBOOK 必須記錄 basis、runs、deploy marker 與 gate 邊界。",
|
||
},
|
||
{
|
||
"rule_id": "memory_index_startup_exception",
|
||
"priority": "P1",
|
||
"rule": "repo-local MEMORY.md 缺檔時,需記錄使用全域 memory 與 LOGBOOK 的替代讀取路徑。",
|
||
"verification": "啟動記錄不得假稱 repo-local MEMORY.md 已讀。",
|
||
},
|
||
]
|
||
|
||
RULE_ADJUSTMENTS_REQUIRED = [
|
||
{
|
||
"adjustment_id": "low_friction_but_p0_stop_the_bleed",
|
||
"priority": "P0",
|
||
"from_rule": "初期資安低摩擦、先只讀框架。",
|
||
"adjusted_rule": "低摩擦不代表延後修補即時資訊揭露;public surface leak 可先 source-control 止血,再補 owner gate。",
|
||
},
|
||
{
|
||
"adjustment_id": "internal_evidence_not_product_copy",
|
||
"priority": "P0",
|
||
"from_rule": "source-control evidence 可保留技術識別。",
|
||
"adjusted_rule": "技術識別只能留在內部 evidence;產品頁只顯示 redacted scope id、風險等級、狀態與 evidence ref。",
|
||
},
|
||
{
|
||
"adjustment_id": "awooop_approval_not_security_acceptance",
|
||
"priority": "P0",
|
||
"from_rule": "AwoooP 可顯示 owner response / approval 候選。",
|
||
"adjusted_rule": "AwoooP approval、Runs、Work Items、Tenants 顯示全部只算狀態,不等於 IwoooS security acceptance。",
|
||
},
|
||
{
|
||
"adjustment_id": "nginx_config_control_first",
|
||
"priority": "P0",
|
||
"from_rule": "高價值配置逐步納管。",
|
||
"adjusted_rule": "Nginx / public gateway / DNS / TLS 是 C0,優先要求 source-of-truth、drift evidence、owner response、rollback 與 post-check。",
|
||
},
|
||
{
|
||
"adjustment_id": "owner_response_language_not_execution",
|
||
"priority": "P0",
|
||
"from_rule": "owner 可回覆 decision。",
|
||
"adjusted_rule": "即使 owner 文字包含同意、批准、OK,也只算 metadata decision,不自動授權 refs sync、reload、deploy 或 runtime action。",
|
||
},
|
||
{
|
||
"adjustment_id": "public_verification_required_after_frontend_change",
|
||
"priority": "P0",
|
||
"from_rule": "改前端後做 smoke。",
|
||
"adjusted_rule": "改前端或 public API 後必須做 production desktop/mobile sensitive scan、水平溢出檢查與關鍵卡片可見檢查。",
|
||
},
|
||
{
|
||
"adjustment_id": "agent_bounty_c0_runtime_boundary",
|
||
"priority": "P1",
|
||
"from_rule": "agent-bounty-protocol 納入 IwoooS scope。",
|
||
"adjusted_rule": "agent-bounty-protocol 需以 C0 runtime / MCP / A2A / treasury 邊界管理,但保持獨立產品與 owner response。",
|
||
},
|
||
]
|
||
|
||
PRIORITY_WORK_QUEUE = [
|
||
{
|
||
"priority": "P0-1",
|
||
"work_item": "S4.9 owner response gate 收件前置",
|
||
"completion_percent": 70,
|
||
"blocked_by": "尚未人工送件、尚未收到 owner response。",
|
||
"next_validation": "source-control-owner-response-guard。",
|
||
},
|
||
{
|
||
"priority": "P0-2",
|
||
"work_item": "前台 / public API identity redaction",
|
||
"completion_percent": 100,
|
||
"blocked_by": "後續新增頁面仍需 guard 持續保護。",
|
||
"next_validation": "production desktop/mobile sensitive scan。",
|
||
},
|
||
{
|
||
"priority": "P0-3",
|
||
"work_item": "Nginx public gateway owner response acceptance",
|
||
"completion_percent": 86,
|
||
"blocked_by": "尚缺 owner-provided live conf、rendered diff、nginx -t evidence、route smoke、maintenance window、rollback owner。",
|
||
"next_validation": "public gateway acceptance snapshot + owner-provided evidence review。",
|
||
},
|
||
{
|
||
"priority": "P0-4",
|
||
"work_item": "K8s / ArgoCD owner response acceptance",
|
||
"completion_percent": 62,
|
||
"blocked_by": "尚缺 ArgoCD health readback、rendered manifest diff、rollback revision、owner response。",
|
||
"next_validation": "k8s-argocd owner response acceptance snapshot。",
|
||
},
|
||
{
|
||
"priority": "P0-5",
|
||
"work_item": "Backup / restore / escrow owner response acceptance",
|
||
"completion_percent": 62,
|
||
"blocked_by": "尚缺 restore drill approval package、offsite owner、escrow owner、retention owner、validation plan。",
|
||
"next_validation": "backup restore acceptance snapshot。",
|
||
},
|
||
{
|
||
"priority": "P1-1",
|
||
"work_item": "Docker Compose / systemd / host service owner response",
|
||
"completion_percent": 50,
|
||
"blocked_by": "尚缺 110 / 188 live hash、restart window、rollback owner、post-check 指標。",
|
||
"next_validation": "host service owner request / future acceptance ledger。",
|
||
},
|
||
{
|
||
"priority": "P1-2",
|
||
"work_item": "SSH / firewall / network access owner response acceptance",
|
||
"completion_percent": 58,
|
||
"blocked_by": "尚缺 live access state、allowed source CIDR、host key pinning、firewall owner、NetworkPolicy / NodePort / WireGuard owner。",
|
||
"next_validation": "ssh network acceptance snapshot。",
|
||
},
|
||
{
|
||
"priority": "P1-3",
|
||
"work_item": "Monitoring / alerting / observability owner response",
|
||
"completion_percent": 62,
|
||
"blocked_by": "尚缺 live drift evidence、receiver owner、reload owner、route smoke、receipt proof。",
|
||
"next_validation": "monitoring owner request / future acceptance ledger。",
|
||
},
|
||
{
|
||
"priority": "P1-4",
|
||
"work_item": "agent-bounty-protocol C0 runtime / MCP / A2A / treasury owner response",
|
||
"completion_percent": 68,
|
||
"blocked_by": "尚缺 deployment boundary、external agent boundary、treasury owner、runtime gate owner。",
|
||
"next_validation": "agent bounty owner request draft 與 future acceptance ledger。",
|
||
},
|
||
]
|
||
|
||
|
||
def git_output(root: Path, args: list[str], fallback: str = "unknown") -> str:
|
||
try:
|
||
result = subprocess.run(args, cwd=root, check=True, capture_output=True, text=True)
|
||
return result.stdout.strip()
|
||
except Exception:
|
||
return fallback
|
||
|
||
|
||
def load_json(path: Path) -> dict[str, Any]:
|
||
return json.loads(path.read_text(encoding="utf-8"))
|
||
|
||
|
||
def build_report(root: Path, generated_at: str | None) -> dict[str, Any]:
|
||
security_dir = root / "docs" / "security"
|
||
report_time = generated_at or datetime.now(TAIPEI).isoformat(timespec="seconds")
|
||
rollup = load_json(security_dir / "source-control-owner-response-validation-rollup.snapshot.json")
|
||
s49_response = load_json(security_dir / "gitea-inventory-owner-attestation-response.snapshot.json")
|
||
tenants_probe_fields = [
|
||
"source_scope_id",
|
||
"source_namespace_redacted",
|
||
"repo_owner_namespace_redacted",
|
||
"raw_repository_namespace_visible",
|
||
"public_api_raw_repo_namespace_allowed",
|
||
]
|
||
|
||
return {
|
||
"schema_version": "s4_9_owner_response_gap_audit_v1",
|
||
"generated_at": report_time,
|
||
"git_commit": git_output(root, ["git", "rev-parse", "--short", "HEAD"]),
|
||
"status": "gap_audit_ready_owner_gate_zero",
|
||
"mode": "read_only_gap_audit_no_runtime_action",
|
||
"basis": {
|
||
"gitea_main_commit": git_output(root, ["git", "rev-parse", "--short", "gitea/main"]),
|
||
"latest_runtime_deploy_marker": "166497ee",
|
||
"latest_tenants_redaction_commit": "94a9c612",
|
||
"latest_logbook_commit": "57df61da",
|
||
"source_control_rollup_date": rollup.get("date"),
|
||
"source_control_rollup_templates": rollup.get("summary", {}).get("total_response_template_count"),
|
||
"production_verification_required_for_frontend_changes": True,
|
||
},
|
||
"summary": {
|
||
"current_requirement_gap_count": len(CURRENT_REQUIREMENT_GAPS),
|
||
"active_blocker_count": sum(1 for item in CURRENT_REQUIREMENT_GAPS if item["status"] == "active_blocker"),
|
||
"active_risk_or_process_gap_count": sum(
|
||
1 for item in CURRENT_REQUIREMENT_GAPS if item["status"] in {"active_risk", "active_process_gap"}
|
||
),
|
||
"mitigated_needs_guard_count": sum(
|
||
1 for item in CURRENT_REQUIREMENT_GAPS if item["status"] == "mitigated_needs_guard"
|
||
),
|
||
"new_rule_count": len(NEW_RULES_REQUIRED),
|
||
"rule_adjustment_count": len(RULE_ADJUSTMENTS_REQUIRED),
|
||
"priority_work_item_count": len(PRIORITY_WORK_QUEUE),
|
||
"s4_9_owner_response_gate_percent": 0,
|
||
"request_sent_count": 0,
|
||
"owner_response_received_count": s49_response.get("summary", {}).get("received_response_count", 0),
|
||
"owner_response_accepted_count": s49_response.get("summary", {}).get("accepted_response_count", 0),
|
||
"owner_response_rejected_count": s49_response.get("summary", {}).get("rejected_response_count", 0),
|
||
"runtime_gate_count": 0,
|
||
"public_surface_redaction_guard_ready": True,
|
||
"public_surface_raw_namespace_allowed": False,
|
||
"work_session_transcript_public_allowed": False,
|
||
},
|
||
"false_boundaries": FALSE_BOUNDARIES,
|
||
"public_surface_redaction_requirements": {
|
||
"allowed_display": [
|
||
"SRC-### scope id",
|
||
"aggregate count",
|
||
"risk tier",
|
||
"readiness state",
|
||
"redacted evidence ref",
|
||
],
|
||
"forbidden_display": [
|
||
"raw repository owner namespace",
|
||
"personal namespace",
|
||
"external organization namespace when not explicitly public-safe",
|
||
"工作視窗對話內容",
|
||
"approval chat phrase",
|
||
"token / secret / private key / cookie / authorization header",
|
||
],
|
||
"required_fields_or_markers": tenants_probe_fields,
|
||
"verification": [
|
||
"public API payload sensitive scan",
|
||
"production HTML sensitive scan",
|
||
"desktop browser sensitive scan",
|
||
"mobile browser sensitive scan",
|
||
"horizontal overflow check",
|
||
],
|
||
},
|
||
"current_requirement_gaps": CURRENT_REQUIREMENT_GAPS,
|
||
"new_rules_required": NEW_RULES_REQUIRED,
|
||
"rule_adjustments_required": RULE_ADJUSTMENTS_REQUIRED,
|
||
"priority_work_queue": PRIORITY_WORK_QUEUE,
|
||
"next_safe_actions": [
|
||
"更新 S4.9 缺口稽核文件基準與 snapshot reference。",
|
||
"把本 snapshot 納入 source-control owner response guard。",
|
||
"繼續補 owner response acceptance ledger,但所有 request / received / accepted / runtime gate 維持 0 / false。",
|
||
],
|
||
}
|
||
|
||
|
||
def main() -> int:
|
||
parser = argparse.ArgumentParser(description="S4.9 owner response gate 現況缺口稽核 snapshot 產生器")
|
||
parser.add_argument("--root", default=".", help="repo root")
|
||
parser.add_argument("--output", help="寫出 JSON 報告")
|
||
parser.add_argument("--generated-at", help="固定報告時間,供 committed snapshot 使用")
|
||
args = parser.parse_args()
|
||
|
||
root = Path(args.root).resolve()
|
||
report = build_report(root, args.generated_at)
|
||
payload = json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True)
|
||
if args.output:
|
||
output = root / args.output
|
||
output.parent.mkdir(parents=True, exist_ok=True)
|
||
output.write_text(payload + "\n", encoding="utf-8")
|
||
else:
|
||
print(payload)
|
||
|
||
summary = report["summary"]
|
||
print(
|
||
"S4_9_OWNER_RESPONSE_GAP_AUDIT_OK "
|
||
f"gaps={summary['current_requirement_gap_count']} "
|
||
f"new_rules={summary['new_rule_count']} "
|
||
f"adjustments={summary['rule_adjustment_count']} "
|
||
f"owner_gate={summary['s4_9_owner_response_gate_percent']} "
|
||
f"runtime_gate={summary['runtime_gate_count']}",
|
||
file=sys.stderr,
|
||
)
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|