Files
awoooi/scripts/security/s4-9-owner-response-gap-audit.py

398 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
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 responsereviewer 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())