diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 0891880dc..989acce7c 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -1,3 +1,24 @@ +## 2026-05-17 | 資安供應鏈 Progress Guard:AwoooP 只讀進度驗收 + +**背景**:上一輪已把整體進度 58% 寫入 `security_mirror_status_rollup_v1.progress_estimate`。本輪補上可重跑的本機 guard,讓 AwoooP Session 與 Security Supply Chain Session 能用同一個指令確認 58% 只是進度顯示,不是執行授權。 + +**完成**: +- 新增 `scripts/security/security-mirror-progress-guard.py`,只讀 committed snapshots,不呼叫 GitHub / Gitea / Kali / AwoooP / runtime API。 +- `security_mirror_acceptance_v1` 增加 `PROGRESS_ESTIMATE_NOT_AUTHORIZATION` 驗收 check,acceptance checks 從 7 增至 8,blocking checks 從 4 增至 5。 +- 更新 `SECURITY-MIRROR-ACCEPTANCE.md`、`AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md`、`SECURITY-MIRROR-STATUS-ROLLUP.md`、`SECURITY-SUPPLY-CHAIN-PROGRESS.md` 與 AwoooP handoff,要求顯示 58% 時同步顯示 owner response、runtime gate、GitHub primary、workflow / secret inventory 與 production landing 缺口。 + +**Guard 驗收內容**: +- contract count 仍為 35,readiness 與 rollup count 對齊。 +- progress estimate 仍為 overall 58%、framework 80-85%、runtime landing 35-40%,且 `not_authorization=true`。 +- runtime execution、payload ingestion、active runtime gate、GitHub primary switch、repo creation、refs sync、workflow modification、secret value collection 與 action buttons 皆維持 false / 0。 +- owner response received / accepted 仍為 0。 +- acceptance snapshot 必須包含 `PROGRESS_ESTIMATE_NOT_AUTHORIZATION`。 + +**仍禁止**: +- 不把進度估算當成 approval、runtime execution、GitHub primary cutover 或 Kali scan authorization。 +- 不新增 scan、execute、repo、refs、workflow、secret、runner、primary 或 deploy action button。 +- 不把 acceptance guard 轉成 production runtime blocker;它只阻擋不完整或誤授權的 mirror payload。 + ## 2026-05-17 | 資安供應鏈進度估算與雙 Session 同步更新 **背景**:統帥詢問目前整體資安網進度,並再次批准繼續。此階段不能用 runtime enforcement 開啟數量來誤報進度,因為統帥已明確要求初期不要把資安等級一次拉太高;因此本輪把進度估算寫入既有 `security_mirror_status_rollup_v1`,供 AwoooP 與 Security Supply Chain 兩個 Session 用同一份只讀資料同步。 diff --git a/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md b/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md index 5eaf8ccd0..c75fdaaf6 100644 --- a/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md +++ b/docs/security/AWOOOP-MIRROR-ONLY-CONSUMPTION-CHECKLIST.md @@ -38,10 +38,10 @@ AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得 | `security_mirror_intake_plan_v1` | AwoooP mirror-only intake waves / destinations / acceptance gates | Operator Console、Runtime State、Channel Event、Audit、Approval Queue | mirror-only | 只照 wave 讀取與顯示,不執行 intake item | | `security_mirror_event_v1` | AwoooP mirror-only event envelope | Operator Console、Runtime State、Channel Event、Audit、Approval Queue | mirror-only | 每筆 event 必須 `execution_authorized=false`、`action_buttons_allowed=false` | | `security_mirror_route_v1` | AwoooP 鏡像路由矩陣 | Operator Console、Runtime State、Channel Event、Audit、Approval Queue | mirror-only | 只決定目的地、channel policy 與 review lane,不作 execution router | -| `security_mirror_acceptance_v1` | AwoooP 鏡像驗收契約 | Operator Console、Runtime State、Audit | mirror-only | 只驗收 contract count、event envelope、route coverage、redaction;不作 runtime blocker | +| `security_mirror_acceptance_v1` | AwoooP 鏡像驗收契約 | Operator Console、Runtime State、Audit | mirror-only | 只驗收 contract count、event envelope、route coverage、redaction、progress estimate guard;不作 runtime blocker | | `security_mirror_quarantine_v1` | AwoooP 鏡像隔離契約 | Operator Console、Audit | mirror-only | 只隔離驗收失敗 payload、顯示 recovery request 與 retry gate;不作 runtime blocker | | `security_mirror_dry_run_v1` | AwoooP 鏡像 dry-run 報告契約 | Operator Console、Audit | mirror-only | 只回報接入演練結果;不得轉成 production ingestion | -| `security_mirror_status_rollup_v1` | AwoooP 鏡像狀態彙整契約 | Operator Console、Runtime State、Audit | mirror-only | 只顯示階段狀態、下一個 gate 與禁止事項;不得視為 runtime authorization | +| `security_mirror_status_rollup_v1` | AwoooP 鏡像狀態彙整契約 | Operator Console、Runtime State、Audit | mirror-only | 只顯示階段狀態、58% 進度估算、下一個 gate 與禁止事項;不得視為 runtime authorization | | `source_control_owner_response_validation_rollup_v1` | S4.9 / S4.10 / S4.11 / S4.12 owner response validation rollup | Operator Console、Source-control review、Audit | mirror-only | 只顯示四包 response packets、22 個 templates、10 個 cross-packet checks 與 quarantine rules;不得視為 approval 或 runtime gate | | `coding_task_v1` | Code Review / Codex Security / manual review | Approval candidate、Channel Event、Audit | suggest-only | 不自動開 patch runner、不自動 merge | | `source_control_migration_event_v1` | Gitea/GitHub branch/tag/SHA diff | Supply-chain evidence、Approval candidate | mirror-only | 不觸發 deploy、不切換 primary | @@ -108,10 +108,10 @@ AwoooP 初期不得直接啟動掃描、不得呼叫 Codex patch runner、不得 | `security_mirror_intake_plan_v1.status=draft` | `observe` | 顯示 5 個 intake waves 與 4 個 acceptance gates;不得執行 wave | | `security_mirror_event_v1.execution_authorized=false` | `observe` | 只包裝鏡像 payload,明確不授權執行、不顯示執行按鈕 | | `security_mirror_route_v1.status=draft` | `observe` | 顯示 5 個 route groups、channel policy 與 review lane;不得轉成 execution router | -| `security_mirror_acceptance_v1.status=draft` | `observe` | 顯示 7 個 acceptance checks;只可驗收鏡像資料,不得阻擋 runtime | +| `security_mirror_acceptance_v1.status=draft` | `observe` | 顯示 8 個 acceptance checks,其中 progress estimate guard 必須確認 58% 不是執行授權;只可驗收鏡像資料,不得阻擋 runtime | | `security_mirror_quarantine_v1.status=draft` | `observe` | 顯示 5 個 quarantine lanes、recovery request 與 retry gate;不得自動重試失敗 payload | | `security_mirror_dry_run_v1.dry_run_status=contract_defined_not_executed` | `observe` | 顯示 6 個 dry-run steps;不得視為 production ingestion 已啟用 | -| `security_mirror_status_rollup_v1.rollup_status=framework_ready_waiting_approval` | `observe` | 顯示 S0-S4 階段、approval queue summary 與下一個 gate;不得新增 execution action | +| `security_mirror_status_rollup_v1.rollup_status=framework_ready_waiting_approval` | `observe` | 顯示 S0-S4 階段、58% 進度估算、approval queue summary 與下一個 gate;不得新增 execution action | | `source_control_owner_response_validation_rollup_v1.status=draft_waiting_owner_responses` | `observe` | 顯示四包 owner response packets、22 個 templates、received / accepted / rejected 皆為 0;不得當成 approval 或 execution authorization | | `coding_task_v1.risk=LOW|MEDIUM` | `warn` | 可排入 Codex patch-only backlog | | `coding_task_v1.risk=HIGH|CRITICAL` | `approve_required` | 必須指定 `critic`、`vuln-verifier` | diff --git a/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md b/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md index 3429ce93e..8c6ce474a 100644 --- a/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md +++ b/docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md @@ -36,6 +36,14 @@ AwoooP 目前應同步顯示 S4.9-S4.13 owner response 缺口、Gitea authenticated inventory partial 狀態、GitHub primary ready 0/7、workflow / secret inventory complete 0,以及 Kali `/execute` block candidate。這些狀態只供治理與人工審查,不代表 scan、repo 建立、refs sync、workflow 修改、secret 搬移、runner 啟用或 GitHub primary cutover 已獲授權。 +同步驗收時可先跑: + +```text +python3 scripts/security/security-mirror-progress-guard.py +``` + +此腳本只讀 committed snapshots,確認進度估算、contract count、owner response、runtime gate、GitHub primary 與 forbidden actions 仍維持 mirror-only 邊界。 + ## 1. Session 分工 ### AwoooP 主線 Session diff --git a/docs/security/SECURITY-MIRROR-ACCEPTANCE.md b/docs/security/SECURITY-MIRROR-ACCEPTANCE.md index ecb908591..74a14594a 100644 --- a/docs/security/SECURITY-MIRROR-ACCEPTANCE.md +++ b/docs/security/SECURITY-MIRROR-ACCEPTANCE.md @@ -22,6 +22,7 @@ 5. LOW / MEDIUM observation 初期不阻擋。 6. Approval Queue 不等於 execution queue。 7. Channel Event 初期低噪音。 +8. 進度估算只能顯示,不可被解讀成 runtime authorization。 ## 1. 驗收 Checks @@ -31,6 +32,7 @@ | `EVENT_ENVELOPE_REQUIRED` | 確認每筆 payload 都不可執行、不可顯示執行按鈕 | 是 | | `ROUTE_GROUP_COVERAGE` | 確認 5 個 route groups 覆蓋所有 contracts | 是 | | `REDACTION_ONLY` | 確認不保存 raw sensitive value | 是 | +| `PROGRESS_ESTIMATE_NOT_AUTHORIZATION` | 確認 58% 進度估算只作跨 Session 狀態顯示,不授權 scan / execute / repo / refs / workflow / secret / runner / primary 動作 | 是 | | `LOW_MEDIUM_NOT_BLOCKING` | 確認低中風險 observation 初期只 observe / warn | 否 | | `APPROVAL_IS_NOT_EXECUTION` | 確認 approval 只留痕、不自動執行 | 否 | | `CHANNEL_LOW_NOISE` | 確認通知低噪音 | 否 | diff --git a/docs/security/SECURITY-MIRROR-STATUS-ROLLUP.md b/docs/security/SECURITY-MIRROR-STATUS-ROLLUP.md index 6b73aa3f3..a3f8f7c3f 100644 --- a/docs/security/SECURITY-MIRROR-STATUS-ROLLUP.md +++ b/docs/security/SECURITY-MIRROR-STATUS-ROLLUP.md @@ -45,6 +45,12 @@ 因此跨 Session 採用 **58%** 作為目前整體進度。這代表「框架健康、尚未過度收緊」,不是 runtime enforcement 或 primary cutover 授權。 +只讀驗證: + +```text +python3 scripts/security/security-mirror-progress-guard.py +``` + ## 2. AwoooP 可做 1. 顯示 S0 到 S4 的階段狀態。 diff --git a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md index 5c3d66224..50afc15f1 100644 --- a/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md +++ b/docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md @@ -19,6 +19,12 @@ 這個進度估算用於雙 Session 同步與階段判讀,不是 approval、runtime execution、GitHub primary cutover 或 Kali scan authorization。現階段仍維持統帥要求的低摩擦策略:先建完整框架與 evidence,之後再分階段收斂。 +本估算可用以下只讀 guard 驗證: + +```text +python3 scripts/security/security-mirror-progress-guard.py +``` + | 階段 | 狀態 | 目前結果 | 下一個 gate | |------|------|----------|-------------| | S0 文件與契約同步 | 完成 | Kali / Codex / GitHub / Gitea / AwoooP 邊界已文件化,核心 schema 草案已建立 | AwoooP 只讀 mirror 消費 | diff --git a/docs/security/security-mirror-acceptance.snapshot.json b/docs/security/security-mirror-acceptance.snapshot.json index a81b4f6c6..9d7261f12 100644 --- a/docs/security/security-mirror-acceptance.snapshot.json +++ b/docs/security/security-mirror-acceptance.snapshot.json @@ -8,14 +8,15 @@ "docs/security/security-mirror-readiness.snapshot.json", "docs/security/security-supply-chain-contract-manifest.snapshot.json", "docs/security/security-mirror-event-sample.snapshot.json", - "docs/security/security-mirror-route.snapshot.json" + "docs/security/security-mirror-route.snapshot.json", + "docs/security/security-mirror-status-rollup.snapshot.json" ], "summary": { "total_contracts": 35, "ready_for_mirror_count": 32, "route_group_count": 5, - "acceptance_check_count": 7, - "blocking_check_count": 4 + "acceptance_check_count": 8, + "blocking_check_count": 5 }, "acceptance_checks": [ { @@ -93,6 +94,29 @@ "將 secret value 寫入 Runtime State 或 Audit evidence" ] }, + { + "check_id": "PROGRESS_ESTIMATE_NOT_AUTHORIZATION", + "title": "進度估算不是執行授權", + "expected_result": "AwoooP 顯示 `security_mirror_status_rollup_v1.progress_estimate.overall_percent=58`,且 `not_authorization=true`、`runtime_execution_authorized=false`、`active_runtime_gate_count=0`、`github_primary_ready_count=0`。", + "evidence_refs": [ + "docs/security/security-mirror-status-rollup.snapshot.json", + "docs/security/SECURITY-MIRROR-STATUS-ROLLUP.md", + "docs/security/SECURITY-SUPPLY-CHAIN-PROGRESS.md", + "docs/security/AWOOOP-SECURITY-SUPPLYCHAIN-INTEGRATION-HANDOFF.md" + ], + "blocking_if_failed": true, + "allowed_processing": [ + "顯示整體 58%、框架 80-85%、落地 35-40%", + "顯示 owner response、runtime gate、GitHub primary 與 AwoooP production ingestion 缺口", + "要求來源修正 rollup 後再 mirror" + ], + "blocked_processing": [ + "把 58% 視為 approval", + "把 58% 視為 runtime authorization", + "隱藏 owner response 或 runtime landing 缺口", + "新增 scan / execute / repo / refs / workflow / secret / runner / primary action button" + ] + }, { "check_id": "LOW_MEDIUM_NOT_BLOCKING", "title": "LOW / MEDIUM 不升級為阻擋", diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py new file mode 100755 index 000000000..6c5437a0d --- /dev/null +++ b/scripts/security/security-mirror-progress-guard.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Validate the mirror-only security progress guardrails. + +This script is intentionally read-only. It checks committed snapshots only and +does not call GitHub, Gitea, Kali, AwoooP, or any runtime API. +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from typing import Any + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def assert_equal(label: str, actual: Any, expected: Any) -> None: + if actual != expected: + raise SystemExit(f"BLOCKED {label}: expected {expected!r}, got {actual!r}") + + +def assert_false(label: str, actual: Any) -> None: + assert_equal(label, actual, False) + + +def assert_true(label: str, actual: Any) -> None: + assert_equal(label, actual, True) + + +def assert_contains(label: str, values: list[Any], expected: Any) -> None: + if expected not in values: + raise SystemExit(f"BLOCKED {label}: missing {expected!r}") + + +def validate(root: Path) -> None: + security_dir = root / "docs" / "security" + + manifest = load_json(security_dir / "security-supply-chain-contract-manifest.snapshot.json") + readiness = load_json(security_dir / "security-mirror-readiness.snapshot.json") + rollup = load_json(security_dir / "security-mirror-status-rollup.snapshot.json") + acceptance = load_json(security_dir / "security-mirror-acceptance.snapshot.json") + owner_rollup = load_json(security_dir / "source-control-owner-response-validation-rollup.snapshot.json") + primary_gate = load_json(security_dir / "source-control-primary-readiness-gate.snapshot.json") + + manifest_count = manifest["contract_count"] + readiness_summary = readiness["summary"] + rollup_summary = rollup["summary"] + + assert_equal("manifest.contract_count", manifest_count, 35) + assert_equal("readiness.total_contracts", readiness_summary["total_contracts"], manifest_count) + assert_equal("rollup.total_contracts", rollup_summary["total_contracts"], manifest_count) + assert_equal("rollup.ready_for_mirror_count", rollup_summary["ready_for_mirror_count"], 32) + assert_equal("rollup.partial_ready_count", rollup_summary["partial_ready_count"], 2) + assert_equal("rollup.contract_only_count", rollup_summary["contract_only_count"], 1) + assert_equal("rollup.blocked_count", rollup_summary["blocked_count"], 0) + + progress = rollup["progress_estimate"] + assert_equal("progress.overall_percent", progress["overall_percent"], 58) + assert_equal("progress.framework_percent_min", progress["framework_percent_min"], 80) + assert_equal("progress.framework_percent_max", progress["framework_percent_max"], 85) + assert_equal("progress.runtime_landing_percent_min", progress["runtime_landing_percent_min"], 35) + assert_equal("progress.runtime_landing_percent_max", progress["runtime_landing_percent_max"], 40) + assert_true("progress.not_authorization", progress["not_authorization"]) + + assert_false("rollup.runtime_execution_authorized", rollup["runtime_execution_authorized"]) + assert_equal("rollup.active_runtime_gate_count", rollup_summary["active_runtime_gate_count"], 0) + assert_false("rollup.runtime_actions_executed", rollup_summary["runtime_actions_executed"]) + assert_false("rollup.payloads_ingested", rollup_summary["payloads_ingested"]) + assert_equal("rollup.github_primary_ready_count", rollup_summary["github_primary_ready_count"], 0) + assert_equal("rollup.owner_response_validation_received_count", rollup_summary["owner_response_validation_received_count"], 0) + assert_equal("rollup.owner_response_validation_accepted_count", rollup_summary["owner_response_validation_accepted_count"], 0) + assert_equal("rollup.workflow_secret_inventory_complete_count", rollup_summary["workflow_secret_inventory_complete_count"], 0) + assert_false("rollup.secret_value_collection_allowed", rollup_summary["secret_value_collection_allowed"]) + assert_false("rollup.secret_value_detected", rollup_summary["secret_value_detected"]) + + owner_summary = owner_rollup["summary"] + assert_equal("owner_rollup.total_received_response_count", owner_summary["total_received_response_count"], 0) + assert_equal("owner_rollup.total_accepted_response_count", owner_summary["total_accepted_response_count"], 0) + assert_false("owner_rollup.runtime_execution_authorized", owner_summary["runtime_execution_authorized"]) + assert_false("owner_rollup.repo_creation_authorized", owner_summary["repo_creation_authorized"]) + assert_false("owner_rollup.refs_sync_authorized", owner_summary["refs_sync_authorized"]) + assert_false("owner_rollup.workflow_modification_authorized", owner_summary["workflow_modification_authorized"]) + assert_false("owner_rollup.github_primary_switch_authorized", owner_summary["github_primary_switch_authorized"]) + assert_false("owner_rollup.action_buttons_allowed", owner_summary["action_buttons_allowed"]) + + primary_summary = primary_gate["summary"] + assert_equal("primary_gate.primary_ready_count", primary_summary["primary_ready_count"], 0) + assert_false("primary_gate.runtime_actions_authorized", primary_summary["runtime_actions_authorized"]) + assert_false("primary_gate.github_primary_switch_authorized", primary_summary["github_primary_switch_authorized"]) + assert_false("primary_gate.action_buttons_allowed", primary_summary["action_buttons_allowed"]) + assert_false("primary_gate.raw_secret_storage_authorized", primary_summary["raw_secret_storage_authorized"]) + + acceptance_ids = [item["check_id"] for item in acceptance["acceptance_checks"]] + assert_contains("acceptance_checks", acceptance_ids, "PROGRESS_ESTIMATE_NOT_AUTHORIZATION") + assert_equal("acceptance.summary.acceptance_check_count", acceptance["summary"]["acceptance_check_count"], len(acceptance_ids)) + assert_equal( + "acceptance.summary.blocking_check_count", + acceptance["summary"]["blocking_check_count"], + sum(1 for item in acceptance["acceptance_checks"] if item["blocking_if_failed"]), + ) + assert_false("acceptance.runtime_execution_authorized", acceptance["runtime_execution_authorized"]) + + forbidden_actions = set(rollup["forbidden_actions"]) | set(acceptance["forbidden_actions"]) + for action in [ + "start_kali_scan", + "call_kali_execute_endpoint", + "create_github_repo", + "change_repo_visibility", + "sync_git_refs", + "switch_github_primary", + "production_deploy", + ]: + assert_contains("forbidden_actions", list(forbidden_actions), action) + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--root", + default=Path(__file__).resolve().parents[2], + type=Path, + help="Repository root. Defaults to the current script's repository.", + ) + args = parser.parse_args() + validate(args.root.resolve()) + print("SECURITY_MIRROR_PROGRESS_GUARD_OK") + + +if __name__ == "__main__": + main()