diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 497b8403..533bfb13 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -166,7 +166,7 @@ - Mac Mini / MacBook 的 `~/.codex/CODEX-START-HERE.md` 與 `codex-workstation-sync-dashboard.snapshot.json` 已從舊 `V10.651` / MacBook blocked wording 校準為 current-main baseline;仍未同步 auth、SQLite、sessions、raw conversations、`.env`、runtime volumes、raw `.git`。 **Wazuh 分工邊界**: -- IwoooS 主控視窗同步的 Wazuh 只讀 API 邊界 Wazuh API commit 是 `47d36e85`;最終分支 HEAD 與 release patch set SHA-256 需在 final docs commit 後以 `git rev-parse HEAD`、`git format-patch gitea/main..HEAD`、`shasum -a 256` 讀回,避免 committed 文件自我引用造成 hash 漂移。 +- IwoooS 主控視窗同步的 Wazuh 只讀 API 邊界已改為 release 前 readback 模式;Wazuh API commit、最終分支 HEAD 與 release patch set SHA-256 需在 final docs commit 後以 `git log --oneline gitea/main..HEAD`、`git rev-parse HEAD`、`git format-patch gitea/main..HEAD`、`shasum -a 256` 讀回,避免 rebase 後 hash 漂移。 - 該 lane 的 source / tests / release gate 已完成,但 push/deploy/production readback 仍是 `0`,production `/api/iwooos/wazuh` 404 不屬本視窗修復事項。 - 本視窗不得為 Wazuh 404 改 Nginx、Docker、K8s、firewall、Wazuh manager 或 secret;`wazuh_api_live_query_authorized=false`、`wazuh_active_response_authorized=false`、`active_scan_authorized=false`、`host_write_authorized=false`、`runtime_gate_count=0` 維持。 @@ -240,13 +240,21 @@ **Release handoff 補充**:受控 workspace 的 Gitea HTTPS push 因非互動式 credential 缺失失敗;本輪未複製或使用舊 workspace 內嵌明文 token。已新增 `docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md`,供具備正式 Gitea / release 權限的 lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD 或同等 patch,並以 production `/api/iwooos/wazuh` readback 驗證不再 404。 **Release apply proof 補充,21:58 Asia/Taipei**: -- Wazuh API commit:`47d36e85 fix(iwooos): 接上 Wazuh 只讀 API 邊界`;最終分支 HEAD 與 release patch set SHA-256 不硬寫進 committed 文件,需在 final docs commit 後以命令讀回。 -- 已從最新 `gitea/main=80604403` 建立獨立 worktree 並套用 patch set 成功;後續若文件 commit 再變動,release 執行者需重新 `git format-patch gitea/main..HEAD` 與 apply-check,避免沿用舊 patch SHA。 +- Wazuh API commit、最終分支 HEAD 與 release patch set SHA-256 不硬寫進 committed 文件,需在 final docs commit 後以命令讀回。 +- 已從當時最新 `gitea/main` 建立獨立 worktree 並套用 patch set 成功;後續若主線或文件 commit 再變動,release 執行者需重新 `git format-patch gitea/main..HEAD` 與 apply-check,避免沿用舊 patch SHA。 - 乾淨套用 worktree 通過 `pytest apps/api/tests/test_iwooos_wazuh_api.py`、`wazuh-readonly-route-boundary-guard.py`、`wazuh-readonly-release-gate.py`、`security-mirror-progress-guard.py`、`doc-secrets-sanity-check.py`、`py_compile` 與 `git diff --check`。 - `docs/security/wazuh-readonly-release-gate.snapshot.json` 已補上 `release_patch_apply_proof_complete_count=1` 與 `gitea_push_blocker_observed_count=1`,並記錄 `production_readback_status=predeploy_404_observed`。 - 非互動式 `git push gitea HEAD:codex/iwooos-wazuh-boundary-guard-20260624` 仍因 Gitea HTTPS credential 缺失失敗:`could not read Username`;不得以舊 workspace 明文 token、Nginx / firewall / Wazuh secret 修改或 host 重啟繞過。 - Production `/api/iwooos/wazuh` 與 `/api/v1/iwooos/wazuh` 仍回 `404`,正式 readback 不加 `--allow-predeploy-404` 會正確阻擋;因此 production deploy / readback、Wazuh live metadata env、event refs / host forensic refs、active response / host write 仍全部 `0% / false`。 +**Release lane preflight 補充,22:20 Asia/Taipei**: +- `gitea/main` 已前進到 `20cb3e16 docs(ops): record momo import boundary hardening [skip ci]`;Wazuh 分支已 rebase 到此基底,仍只比主線多 Wazuh source / release evidence commits。 +- 新增 `scripts/security/wazuh-readonly-release-lane-preflight.py` 與 `docs/security/wazuh-readonly-release-lane-preflight.snapshot.json`,並接入 `security-mirror-progress-guard.py`。 +- Preflight 固定三條合規 release lane:`formal_gitea_merge`、`formal_patch_apply`、`maintainer_local_push_with_safe_credential`;目前 `formal_release_lane_ready_count=0`、ack `0/6`、evidence `0/6`。 +- 明確阻擋:明文 Gitea token remote、從髒 workspace 複製 token、force push、Nginx / Docker / K8s / firewall workaround、Wazuh secret / manager 變更、未經 owner gate 啟用 live metadata、Wazuh active response、host write、Kali active scan。 +- 完成度:release lane preflight artifact / guard `100%`;owner acks / evidence `0%`;Gitea push / production deploy / production readback / runtime gate 仍 `0%`。 +- 邊界:本段沒有讀 git credential、沒有推送、沒有部署、沒有 Wazuh live query、沒有 host write、沒有 runtime action;只是把 release blocker 變成可審核 gate。 + ## 2026-06-24|21:04 recovery readback 與 MOMO V10.651 雙機基準收斂 **背景**:前一輪 MOMO workspace readback 指到 `V10.646`,但 21:04 live health 已回 `V10.651`。因此本輪重新比對 Gitea `wooo/ewoooc` `main`、正式站 `/health`、Mac Mini / MacBook Pro Codex workspace 與 full-stack cold-start,避免「網站可用」和「版本 / 資料最新」互相混淆。 diff --git a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md index 00580172..f6964d21 100644 --- a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md +++ b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md @@ -18,7 +18,7 @@ 本地 commit: -- `codex/iwooos-wazuh-boundary-guard-20260624` Wazuh API commit:`47d36e85 fix(iwooos): 接上 Wazuh 只讀 API 邊界` +- `codex/iwooos-wazuh-boundary-guard-20260624` Wazuh API commit 會在每次 rebase 後改變;請在 release 前用 `git log --oneline gitea/main..HEAD` 讀回,不硬寫在本文件內。 - `codex/iwooos-wazuh-boundary-guard-20260624` 最終分支 HEAD 不硬寫在本文件內;請在 release 前用 `git rev-parse HEAD` 讀回,避免 commit 自我引用造成 hash 漂移。 - Release patch set 需在最終 docs commit 後以 `git format-patch gitea/main..HEAD` 重新產生,再用 `shasum -a 256` 讀回;不得沿用 rebase 前或文件修正前的舊 patch SHA。 @@ -30,8 +30,10 @@ - `scripts/security/wazuh-readonly-route-boundary-guard.py` - `scripts/security/wazuh-readonly-production-readback.py` - `scripts/security/wazuh-readonly-release-gate.py` +- `scripts/security/wazuh-readonly-release-lane-preflight.py` - `scripts/security/security-mirror-progress-guard.py` - `docs/security/wazuh-readonly-release-gate.snapshot.json` +- `docs/security/wazuh-readonly-release-lane-preflight.snapshot.json` - `docs/LOGBOOK.md` 完成內容: @@ -46,6 +48,7 @@ - 新增 source guard,阻擋硬編 Wazuh 內網 URL / port、帳密、關 TLS、假 SOC dashboard、假 CVE、raw payload 與 legacy dashboard component 回流。 - 新增 production readback 腳本,部署後可直接驗證 public API 不再 404、schema / status / boundary 正確,且沒有 raw payload、內網 IP、agent 原名或 secret 洩漏。 - 新增 release gate snapshot 與 guard,固定 source-side 已完成、Gitea push / production deploy / production readback 尚未完成,避免後續把 predeploy 404 誤判成通過。 +- 新增 release lane preflight snapshot 與 guard,固定正式 release 前必須選擇 `formal_gitea_merge`、`formal_patch_apply` 或 `maintainer_local_push_with_safe_credential` 其中一條合規 lane,且 owner ack / evidence 未到齊前不得 push、deploy、force push、使用明文 token workaround 或改 runtime。 ## 已完成驗證 @@ -55,9 +58,10 @@ pytest apps/api/tests/test_iwooos_wazuh_api.py python3 scripts/security/wazuh-readonly-route-boundary-guard.py --root . python3 scripts/security/wazuh-readonly-release-gate.py --root . +python3 scripts/security/wazuh-readonly-release-lane-preflight.py --root . python3 scripts/security/security-mirror-progress-guard.py --root . -python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py -python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/security-mirror-progress-guard.py +python3 scripts/ops/doc-secrets-sanity-check.py docs apps/api/src/api/v1/iwooos.py apps/web/src/app/api/iwooos/wazuh/route.ts scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py +python3 -m py_compile apps/api/src/api/v1/iwooos.py scripts/security/wazuh-readonly-route-boundary-guard.py scripts/security/wazuh-readonly-production-readback.py scripts/security/wazuh-readonly-release-gate.py scripts/security/wazuh-readonly-release-lane-preflight.py scripts/security/security-mirror-progress-guard.py git diff --check ``` @@ -66,6 +70,7 @@ git diff --check - `pytest apps/api/tests/test_iwooos_wazuh_api.py`:`4 passed`。 - `wazuh-readonly-route-boundary-guard`:`route=2 public_ui_files=1 forbidden=0 runtime_gate=0`。 - `wazuh-readonly-release-gate`:`source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 +- `wazuh-readonly-release-lane-preflight`:`ready=0 acks=0/6 evidence=0/6 runtime_gate=0`。 - `security-mirror-progress-guard`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 - `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=969`。 - `py_compile`:通過。 @@ -73,7 +78,7 @@ git diff --check ## 乾淨套用 Proof -乾淨套用 proof 需從最新 `gitea/main=80604403` 或更新的主線建立獨立 worktree: +乾淨套用 proof 需從最新 `gitea/main` 建立獨立 worktree: ```bash git worktree add /private/tmp/awoooi-iwooos-wazuh-release-apply-check- gitea/main @@ -113,6 +118,7 @@ python3 scripts/security/wazuh-readonly-production-readback.py --allow-predeploy 合併 / 部署前需確認: - 使用具備正式權限的 Gitea lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD 或同等 patch;不得 force push。 +- release lane preflight 目前固定 `formal_release_lane_ready_count=0`、`accepted_ack_flag_count=0/6`、`accepted_evidence_field_count=0/6`;不得把一般「批准繼續」當成 release lane owner response。 - 目前非互動式 push 實測仍被 Gitea HTTPS credential 擋住:`fatal: could not read Username for 'https://gitea.wooo.work': terminal prompts disabled`。 - 不得複製舊 workspace 的內嵌明文 Gitea token。 - 不得把 Wazuh URL、帳密、token、cookie、private key、runner token 或 webhook secret 寫入 repo。 @@ -161,6 +167,7 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json | Wazuh route boundary source guard | `100%` | 已納入 `security-mirror-progress-guard` | | Production readback 驗收腳本 | `100%` | 已完成;正式部署後不得接受 404 | | Wazuh release gate snapshot / guard | `100%` | 已完成;固定 push/deploy/readback 仍 blocked | +| Wazuh release lane preflight | `100%` | 已完成;owner acks `0/6`、evidence `0/6`、正式 release ready `0` | | 乾淨套用 proof | `100%` | patch set 可落在最新 `gitea/main` 並通過同組 guard;最終 hash 以 release 前 readback 為準 | | Gitea push | `0%` | 受控 workspace HTTPS credential 缺失 | | Production deploy / readback | `0%` | 等待 release lane | @@ -170,8 +177,9 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json ## 下一步優先序 -1. 解決受控 workspace Gitea HTTPS push 認證,或由正式 release lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD。 -2. 部署後先驗證 `/api/iwooos/wazuh` 不再 404,且預設 disabled 邊界正確。 -3. 另開 owner gate 決定是否啟用 server-side Wazuh read-only metadata query。 -4. 收件 Wazuh manager health ref、agent status ref、event refs、host forensic refs 與 containment / recovery proof。 -5. 仍禁止 active response、host write、firewall / Nginx / Docker / K8s runtime action、Kali active scan、secret 明文收集。 +1. 先補 release lane owner response:選擇 formal merge、formal patch apply 或安全 credential push,並補 6 個 ack 與 6 個 evidence 欄位。 +2. 解決受控 workspace Gitea HTTPS push 認證,或由正式 release lane 合併 `codex/iwooos-wazuh-boundary-guard-20260624` 分支 HEAD。 +3. 部署後先驗證 `/api/iwooos/wazuh` 不再 404,且預設 disabled 邊界正確。 +4. 另開 owner gate 決定是否啟用 server-side Wazuh read-only metadata query。 +5. 收件 Wazuh manager health ref、agent status ref、event refs、host forensic refs 與 containment / recovery proof。 +6. 仍禁止 active response、host write、firewall / Nginx / Docker / K8s runtime action、Kali active scan、secret 明文收集。 diff --git a/docs/security/wazuh-readonly-release-gate.snapshot.json b/docs/security/wazuh-readonly-release-gate.snapshot.json index eaf3d1e6..10923372 100644 --- a/docs/security/wazuh-readonly-release-gate.snapshot.json +++ b/docs/security/wazuh-readonly-release-gate.snapshot.json @@ -14,7 +14,7 @@ "wazuh_active_response_authorized": false, "wazuh_api_live_query_authorized": false }, - "generated_at": "2026-06-24T22:05:00+08:00", + "generated_at": "2026-06-24T22:25:00+08:00", "missing_required_source_paths": [], "mode": "repo_release_gate_no_runtime_no_secret_collection", "operator_interpretation": [ @@ -75,13 +75,13 @@ ], "release_lane_evidence": { "apply_check_status": "passed_external_readback_required_after_final_commit", - "base_commit": "80604403", + "base_commit_readback": "run git rev-parse gitea/main before release; do not hardcode a moving main commit", "base_ref": "gitea/main", "gitea_push_blocker": "https_noninteractive_credential_required", "production_readback_status": "predeploy_404_observed", "release_patch_set_readback": "generate with git format-patch gitea/main..HEAD after the final docs commit, then record sha256 outside the committed file", "source_branch": "codex/iwooos-wazuh-boundary-guard-20260624", - "source_fix_commit": "47d36e85", + "source_fix_commit_readback": "run git log --oneline gitea/main..HEAD before release; do not hardcode a rebase-sensitive commit hash", "source_head_readback": "run git rev-parse HEAD after the final docs commit; do not hardcode a self-referential commit hash" }, "required_source_paths": [ diff --git a/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json b/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json new file mode 100644 index 00000000..73b6bac6 --- /dev/null +++ b/docs/security/wazuh-readonly-release-lane-preflight.snapshot.json @@ -0,0 +1,104 @@ +{ + "allowed_release_methods": [ + "formal_gitea_merge", + "formal_patch_apply", + "maintainer_local_push_with_safe_credential" + ], + "blocked_actions": [ + "plain_text_gitea_token_in_remote_url", + "copy_token_from_dirty_workspace", + "force_push", + "nginx_or_gateway_workaround_for_404", + "docker_restart_for_wazuh_route", + "k8s_or_argocd_manual_apply_for_wazuh_route", + "firewall_change_for_wazuh_route", + "wazuh_secret_or_manager_change_for_api_404", + "enable_wazuh_live_metadata_without_owner_gate", + "enable_wazuh_active_response", + "host_write_or_kali_active_scan" + ], + "execution_boundaries": { + "force_push_allowed": false, + "host_write_authorized": false, + "kali_active_scan_authorized": false, + "not_authorization": true, + "plain_text_token_workaround_allowed": false, + "production_deploy_authorized": false, + "repo_write_authorized": false, + "runtime_execution_authorized": false, + "secret_value_collection_allowed": false, + "wazuh_active_response_authorized": false, + "wazuh_api_live_query_authorized": false + }, + "generated_at": "2026-06-24T22:20:00+08:00", + "mode": "repo_preflight_no_secret_no_runtime_no_push", + "operator_interpretation": [ + "此 preflight 通過前,不得把 Gitea credential blocker 視為可繞過。", + "正式 release 可以選 formal merge、formal patch apply 或安全 credential push,但都需要 owner response 與 deploy 後 readback。", + "不得用 Nginx、Docker、K8s、firewall、Wazuh secret 或主機重啟來修 public API 404。", + "Wazuh live metadata 查詢與 active response 是不同 gate;本 preflight 不授權任何 runtime action。" + ], + "post_deploy_readback": { + "command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "must_not_return_http_404": true, + "required": true, + "runtime_gate_expected": 0 + }, + "release_lanes": [ + { + "lane_id": "formal_gitea_merge", + "meaning": "由具備正式 Gitea 權限者合併 Wazuh 分支;不得 force push。", + "runtime_authorized": false, + "status": "blocked_owner_response_required" + }, + { + "lane_id": "formal_patch_apply", + "meaning": "由正式 release lane 套用已驗證 patch set;不得跳過 production readback。", + "runtime_authorized": false, + "status": "blocked_owner_response_required" + }, + { + "lane_id": "maintainer_local_push_with_safe_credential", + "meaning": "只接受安全的 credential helper / SSH key / 正式 release token;不得使用明文 token workaround。", + "runtime_authorized": false, + "status": "blocked_safe_credential_required" + } + ], + "required_ack_flags": [ + "approve_formal_release_lane", + "confirm_no_plaintext_token_workaround", + "confirm_no_force_push", + "confirm_no_runtime_workaround", + "confirm_production_readback_after_deploy", + "confirm_wazuh_live_metadata_requires_separate_owner_gate" + ], + "required_evidence_fields": [ + "release_lane_owner", + "release_method", + "target_branch_or_patch_set", + "post_deploy_readback_command", + "rollback_owner", + "blocked_runtime_actions_ack" + ], + "schema_version": "iwooos_wazuh_readonly_release_lane_preflight_v1", + "status": "blocked_waiting_formal_release_lane_owner_response", + "summary": { + "accepted_ack_flag_count": 0, + "accepted_evidence_field_count": 0, + "allowed_release_method_count": 3, + "force_push_allowed_count": 0, + "formal_release_lane_ready_count": 0, + "gitea_push_authorized_count": 0, + "patch_apply_authorized_count": 0, + "plain_text_token_workaround_allowed_count": 0, + "production_deploy_authorized_count": 0, + "production_readback_passed_count": 0, + "production_readback_required_count": 1, + "required_ack_flag_count": 6, + "required_evidence_field_count": 6, + "runtime_gate_count": 0, + "runtime_workaround_allowed_count": 0, + "safe_credential_available_count": 0, + "wazuh_live_metadata_owner_gate_ready_count": 0 + } +} diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 7e84a139..49524935 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -95,6 +95,10 @@ def validate(root: Path) -> None: str(root / "scripts" / "security" / "wazuh-readonly-release-gate.py") ) wazuh_readonly_release_gate["validate"](root) + wazuh_readonly_release_lane_preflight = runpy.run_path( + str(root / "scripts" / "security" / "wazuh-readonly-release-lane-preflight.py") + ) + wazuh_readonly_release_lane_preflight["validate"](root) telegram_alert_readability_guard = runpy.run_path( str(root / "scripts" / "security" / "telegram-alert-readability-guard.py") ) diff --git a/scripts/security/wazuh-readonly-release-gate.py b/scripts/security/wazuh-readonly-release-gate.py index b4b74a38..262d5361 100644 --- a/scripts/security/wazuh-readonly-release-gate.py +++ b/scripts/security/wazuh-readonly-release-gate.py @@ -43,10 +43,10 @@ def build_report(root: Path, generated_at: str | None = None) -> dict[str, Any]: "mode": "repo_release_gate_no_runtime_no_secret_collection", "release_lane_evidence": { "source_branch": "codex/iwooos-wazuh-boundary-guard-20260624", - "source_fix_commit": "47d36e85", + "source_fix_commit_readback": "run git log --oneline gitea/main..HEAD before release; do not hardcode a rebase-sensitive commit hash", "source_head_readback": "run git rev-parse HEAD after the final docs commit; do not hardcode a self-referential commit hash", "base_ref": "gitea/main", - "base_commit": "80604403", + "base_commit_readback": "run git rev-parse gitea/main before release; do not hardcode a moving main commit", "release_patch_set_readback": "generate with git format-patch gitea/main..HEAD after the final docs commit, then record sha256 outside the committed file", "apply_check_status": "passed_external_readback_required_after_final_commit", "production_readback_status": "predeploy_404_observed", diff --git a/scripts/security/wazuh-readonly-release-lane-preflight.py b/scripts/security/wazuh-readonly-release-lane-preflight.py new file mode 100644 index 00000000..1b5237e1 --- /dev/null +++ b/scripts/security/wazuh-readonly-release-lane-preflight.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +""" +IwoooS Wazuh 只讀 API release lane preflight。 + +本工具只檢查 repo 內 committed snapshot,固定 release 前必須由正式 lane +提供非敏感證據;不讀 git credential、不推送、不部署、不查 Wazuh、不改主機。 +""" + +from __future__ import annotations + +import argparse +import json +import sys +from datetime import datetime, timedelta, timezone +from pathlib import Path +from typing import Any + + +TAIPEI = timezone(timedelta(hours=8)) +SNAPSHOT_PATH = Path("docs/security/wazuh-readonly-release-lane-preflight.snapshot.json") +REQUIRED_ACK_FLAGS = [ + "approve_formal_release_lane", + "confirm_no_plaintext_token_workaround", + "confirm_no_force_push", + "confirm_no_runtime_workaround", + "confirm_production_readback_after_deploy", + "confirm_wazuh_live_metadata_requires_separate_owner_gate", +] +REQUIRED_EVIDENCE_FIELDS = [ + "release_lane_owner", + "release_method", + "target_branch_or_patch_set", + "post_deploy_readback_command", + "rollback_owner", + "blocked_runtime_actions_ack", +] +ALLOWED_RELEASE_METHODS = [ + "formal_gitea_merge", + "formal_patch_apply", + "maintainer_local_push_with_safe_credential", +] + + +def now_iso() -> str: + return datetime.now(TAIPEI).replace(microsecond=0).isoformat() + + +def build_report(generated_at: str | None = None) -> dict[str, Any]: + return { + "schema_version": "iwooos_wazuh_readonly_release_lane_preflight_v1", + "generated_at": generated_at or now_iso(), + "status": "blocked_waiting_formal_release_lane_owner_response", + "mode": "repo_preflight_no_secret_no_runtime_no_push", + "required_ack_flags": REQUIRED_ACK_FLAGS, + "required_evidence_fields": REQUIRED_EVIDENCE_FIELDS, + "allowed_release_methods": ALLOWED_RELEASE_METHODS, + "summary": { + "required_ack_flag_count": len(REQUIRED_ACK_FLAGS), + "accepted_ack_flag_count": 0, + "required_evidence_field_count": len(REQUIRED_EVIDENCE_FIELDS), + "accepted_evidence_field_count": 0, + "allowed_release_method_count": len(ALLOWED_RELEASE_METHODS), + "formal_release_lane_ready_count": 0, + "safe_credential_available_count": 0, + "patch_apply_authorized_count": 0, + "gitea_push_authorized_count": 0, + "production_deploy_authorized_count": 0, + "production_readback_required_count": 1, + "production_readback_passed_count": 0, + "plain_text_token_workaround_allowed_count": 0, + "force_push_allowed_count": 0, + "runtime_workaround_allowed_count": 0, + "wazuh_live_metadata_owner_gate_ready_count": 0, + "runtime_gate_count": 0, + }, + "release_lanes": [ + { + "lane_id": "formal_gitea_merge", + "status": "blocked_owner_response_required", + "meaning": "由具備正式 Gitea 權限者合併 Wazuh 分支;不得 force push。", + "runtime_authorized": False, + }, + { + "lane_id": "formal_patch_apply", + "status": "blocked_owner_response_required", + "meaning": "由正式 release lane 套用已驗證 patch set;不得跳過 production readback。", + "runtime_authorized": False, + }, + { + "lane_id": "maintainer_local_push_with_safe_credential", + "status": "blocked_safe_credential_required", + "meaning": "只接受安全的 credential helper / SSH key / 正式 release token;不得使用明文 token workaround。", + "runtime_authorized": False, + }, + ], + "blocked_actions": [ + "plain_text_gitea_token_in_remote_url", + "copy_token_from_dirty_workspace", + "force_push", + "nginx_or_gateway_workaround_for_404", + "docker_restart_for_wazuh_route", + "k8s_or_argocd_manual_apply_for_wazuh_route", + "firewall_change_for_wazuh_route", + "wazuh_secret_or_manager_change_for_api_404", + "enable_wazuh_live_metadata_without_owner_gate", + "enable_wazuh_active_response", + "host_write_or_kali_active_scan", + ], + "post_deploy_readback": { + "required": True, + "command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "must_not_return_http_404": True, + "runtime_gate_expected": 0, + }, + "execution_boundaries": { + "not_authorization": True, + "secret_value_collection_allowed": False, + "plain_text_token_workaround_allowed": False, + "force_push_allowed": False, + "repo_write_authorized": False, + "production_deploy_authorized": False, + "runtime_execution_authorized": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "host_write_authorized": False, + "kali_active_scan_authorized": False, + }, + "operator_interpretation": [ + "此 preflight 通過前,不得把 Gitea credential blocker 視為可繞過。", + "正式 release 可以選 formal merge、formal patch apply 或安全 credential push,但都需要 owner response 與 deploy 後 readback。", + "不得用 Nginx、Docker、K8s、firewall、Wazuh secret 或主機重啟來修 public API 404。", + "Wazuh live metadata 查詢與 active response 是不同 gate;本 preflight 不授權任何 runtime action。", + ], + } + + +def load_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) + + +def validate(root: Path) -> None: + snapshot_path = root / SNAPSHOT_PATH + if not snapshot_path.exists(): + raise SystemExit( + f"BLOCKED Wazuh release lane preflight snapshot missing: {SNAPSHOT_PATH.as_posix()}" + ) + snapshot = load_json(snapshot_path) + expected = build_report(snapshot.get("generated_at")) + + if snapshot.get("schema_version") != expected["schema_version"]: + raise SystemExit("BLOCKED Wazuh release lane preflight schema_version mismatch") + if snapshot.get("status") != expected["status"]: + raise SystemExit("BLOCKED Wazuh release lane preflight status mismatch") + for key, expected_value in expected["summary"].items(): + actual = snapshot.get("summary", {}).get(key) + if actual != expected_value: + raise SystemExit( + f"BLOCKED Wazuh release lane preflight summary.{key}: " + f"expected {expected_value!r}, got {actual!r}" + ) + for key, value in snapshot.get("execution_boundaries", {}).items(): + if key == "not_authorization": + if value is not True: + raise SystemExit("BLOCKED Wazuh release lane preflight not_authorization must be true") + elif value is not False: + raise SystemExit(f"BLOCKED Wazuh release lane preflight execution_boundaries.{key}: expected false") + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 API release lane preflight") + parser.add_argument("--root", default=".", help="repository 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(args.generated_at) + if args.output: + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(report, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8") + validate(root) + summary = report["summary"] + print( + "WAZUH_READONLY_RELEASE_LANE_PREFLIGHT_OK " + f"ready={summary['formal_release_lane_ready_count']} " + f"acks={summary['accepted_ack_flag_count']}/{summary['required_ack_flag_count']} " + f"evidence={summary['accepted_evidence_field_count']}/{summary['required_evidence_field_count']} " + f"runtime_gate={summary['runtime_gate_count']}" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main())