diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 3d7ec245..1cde32fe 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -263,6 +263,13 @@ - 完成度:rebase / snapshot refresh `100%`;formal release lane owner acks `0/6`、evidence `0/6`;Gitea push / production deploy / production readback `0%`。 - 邊界:本段沒有讀 git credential、沒有推送、沒有部署、沒有 live Wazuh query、沒有 Nginx / Docker / K8s / firewall / host / Wazuh secret 變更。 +**Release owner request / acceptance 補充,22:32 Asia/Taipei**: +- 新增 `scripts/security/wazuh-readonly-release-owner-request.py`、`docs/security/wazuh-readonly-release-owner-request.snapshot.json`、`scripts/security/wazuh-readonly-release-owner-response-acceptance.py`、`docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json`,並接入 `security-mirror-progress-guard.py`。 +- Owner request 草稿固定 required ack flags `6`、required evidence fields `6`、allowed release methods `3`、forbidden payloads `12`、blocked actions `11`;目前 request sent `0`、owner response accepted `0`、runtime gate `0`。 +- Owner response acceptance 帳本固定 reviewer checks `15`、outcome lanes `10`、blocked actions `13`;目前 received `0`、accepted `0`、acks `0/6`、evidence `0/6`、formal release ready `0`。 +- 完成度:release owner request / acceptance artifact 與 guard `100%`;正式 owner response / release ready / push / deploy / production readback `0%`。 +- 邊界:本段沒有發送 request、沒有收件、沒有讀 credential、沒有推送、沒有部署、沒有 Wazuh live query、沒有 runtime action;一般「批准繼續」仍不可當 release lane owner response。 + ## 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 5e4d36f2..90a0d9aa 100644 --- a/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md +++ b/docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md @@ -31,9 +31,13 @@ - `scripts/security/wazuh-readonly-production-readback.py` - `scripts/security/wazuh-readonly-release-gate.py` - `scripts/security/wazuh-readonly-release-lane-preflight.py` +- `scripts/security/wazuh-readonly-release-owner-request.py` +- `scripts/security/wazuh-readonly-release-owner-response-acceptance.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/security/wazuh-readonly-release-owner-request.snapshot.json` +- `docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json` - `docs/LOGBOOK.md` 完成內容: @@ -49,6 +53,7 @@ - 新增 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。 +- 新增 release owner request 草稿與 owner response acceptance 帳本,將 required ack flags、required evidence fields、allowed release methods、blocked actions、forbidden payloads 與 reviewer checks 機器可讀化;目前 request sent、response received / accepted、release ready、runtime gate 全部維持 `0`。 ## 已完成驗證 @@ -59,9 +64,11 @@ 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/wazuh-readonly-release-owner-request.py --root . +python3 scripts/security/wazuh-readonly-release-owner-response-acceptance.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 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 +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 scripts/security/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.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/wazuh-readonly-release-owner-request.py scripts/security/wazuh-readonly-release-owner-response-acceptance.py scripts/security/security-mirror-progress-guard.py git diff --check ``` @@ -71,8 +78,10 @@ git diff --check - `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`。 +- `wazuh-readonly-release-owner-request`:`drafts=1 sent=0 accepted=0 runtime_gate=0`。 +- `wazuh-readonly-release-owner-response-acceptance`:`received=0 accepted=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=970`。 +- `doc-secrets-sanity-check`:`DOC_SECRET_SANITY_OK scanned_files=972`。 - `py_compile`:通過。 - `git diff --check`:通過。 @@ -94,7 +103,7 @@ git am /private/tmp/awoooi-iwooos-wazuh-boundary-release-patch-/*.pat - `python3 scripts/security/wazuh-readonly-release-gate.py --root .`:`WAZUH_READONLY_RELEASE_GATE_OK source=1 push=0 deploy=0 readback=0 runtime_gate=0`。 - `python3 scripts/security/wazuh-readonly-release-lane-preflight.py --root .`:`WAZUH_READONLY_RELEASE_LANE_PREFLIGHT_OK ready=0 acks=0/6 evidence=0/6 runtime_gate=0`。 - `python3 scripts/security/security-mirror-progress-guard.py --root .`:`SECURITY_MIRROR_PROGRESS_GUARD_OK`。 -- `python3 scripts/ops/doc-secrets-sanity-check.py ...`:`DOC_SECRET_SANITY_OK scanned_files=970`。 +- `python3 scripts/ops/doc-secrets-sanity-check.py ...`:`DOC_SECRET_SANITY_OK scanned_files=972`。 - `python3 -m py_compile ...`:通過。 - `git diff --check`:通過。 @@ -169,6 +178,7 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json | 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` | +| Wazuh release owner request / acceptance | `100%` | 已完成只讀草稿與收件帳本;request sent `0`、response accepted `0` | | 乾淨套用 proof | `100%` | patch set 可落在最新 `gitea/main` 並通過同組 guard;最終 hash 以 release 前 readback 為準 | | Gitea push | `0%` | 受控 workspace HTTPS credential 缺失 | | Production deploy / readback | `0%` | 等待 release lane | @@ -178,7 +188,7 @@ python3 scripts/security/wazuh-readonly-production-readback.py --json ## 下一步優先序 -1. 先補 release lane owner response:選擇 formal merge、formal patch apply 或安全 credential push,並補 6 個 ack 與 6 個 evidence 欄位。 +1. 先依 `wazuh-readonly-release-owner-request.snapshot.json` 補 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。 diff --git a/docs/security/wazuh-readonly-release-owner-request.snapshot.json b/docs/security/wazuh-readonly-release-owner-request.snapshot.json new file mode 100644 index 00000000..303bb9e8 --- /dev/null +++ b/docs/security/wazuh-readonly-release-owner-request.snapshot.json @@ -0,0 +1,137 @@ +{ + "execution_boundaries": { + "dispatch_authorized": false, + "force_push_allowed": false, + "gitea_push_authorized": false, + "host_write_authorized": false, + "kali_active_scan_authorized": false, + "not_authorization": true, + "patch_apply_authorized": false, + "plain_text_token_workaround_allowed": false, + "production_deploy_authorized": false, + "recipient_confirmed": false, + "repo_write_authorized": false, + "request_sent": 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:32:00+08:00", + "handoff_envelope_fields": [ + "request_id", + "stage_id", + "recipient_role_or_team", + "sender_role_or_team", + "requested_response_window", + "allowed_release_methods", + "required_ack_flags", + "required_evidence_fields", + "target_branch_or_patch_set", + "post_deploy_readback_command", + "forbidden_payloads", + "blocked_runtime_actions", + "followup_owner", + "not_approval" + ], + "mode": "repo_request_draft_no_secret_no_runtime_no_push", + "request_draft": { + "action_buttons_allowed": false, + "allowed_release_methods": [ + "formal_gitea_merge", + "formal_patch_apply", + "maintainer_local_push_with_safe_credential" + ], + "blocked_runtime_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" + ], + "followup_owner": "pending_followup_owner", + "forbidden_payloads": [ + "token", + "secret", + "private_key", + "cookie", + "session", + "authorization_header", + "runner_token", + "webhook_secret", + "wazuh_password", + "wazuh_raw_payload", + "git_credential", + "repo_archive" + ], + "not_approval": true, + "owner_response_accepted": false, + "owner_response_received": false, + "post_deploy_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "recipient_confirmed": false, + "recipient_role_or_team": "pending_release_lane_owner", + "redacted_evidence_refs": [ + "docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md", + "docs/security/wazuh-readonly-release-gate.snapshot.json", + "docs/security/wazuh-readonly-release-lane-preflight.snapshot.json" + ], + "request_id": "iwooos_wazuh_readonly_release_owner_request", + "request_sent": false, + "requested_response_window": "not_scheduled", + "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" + ], + "runtime_gate": false, + "sender_role_or_team": "iwooos_security_reviewer", + "stage_id": "P0-IWOOOS-WAZUH-RELEASE", + "target_branch": "codex/iwooos-wazuh-boundary-guard-20260624", + "target_branch_readback": "git log --oneline gitea/main..HEAD", + "target_patch_set_readback": "git format-patch gitea/main..HEAD after final docs commit; record sha256 outside committed docs" + }, + "schema_version": "iwooos_wazuh_readonly_release_owner_request_v1", + "send_after_conditions": [ + "先確認 gitea/main、Wazuh 分支與另一個 AwoooP Session 基線。", + "只送脫敏欄位與 refs;不得附 secret、raw Wazuh payload、git credential 或 runtime 操作要求。", + "一般批准繼續不是 release owner response。", + "收到 response 後仍需先通過 owner response acceptance ledger,不能直接 push 或 deploy。" + ], + "status": "draft_not_dispatched_waiting_release_lane_owner", + "summary": { + "allowed_release_method_count": 3, + "blocked_action_count": 11, + "forbidden_payload_count": 12, + "formal_release_lane_ready_count": 0, + "gitea_push_authorized_count": 0, + "handoff_envelope_field_count": 14, + "owner_response_accepted_count": 0, + "owner_response_received_count": 0, + "patch_apply_authorized_count": 0, + "production_deploy_authorized_count": 0, + "production_readback_passed_count": 0, + "recipient_confirmed_count": 0, + "request_draft_count": 1, + "request_sent_count": 0, + "required_ack_flag_count": 6, + "required_evidence_field_count": 6, + "runtime_gate_count": 0 + } +} diff --git a/docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json b/docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json new file mode 100644 index 00000000..d1b07d0d --- /dev/null +++ b/docs/security/wazuh-readonly-release-owner-response-acceptance.snapshot.json @@ -0,0 +1,141 @@ +{ + "acceptance_candidate": { + "acceptance_candidate_id": "iwooos_wazuh_readonly_release_owner_response", + "ack_flags": { + "approve_formal_release_lane": false, + "confirm_no_force_push": false, + "confirm_no_plaintext_token_workaround": false, + "confirm_no_runtime_workaround": false, + "confirm_production_readback_after_deploy": false, + "confirm_wazuh_live_metadata_requires_separate_owner_gate": 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", + "mark_general_approval_as_release_response", + "mark_predeploy_404_as_passed_readback" + ], + "decision": "pending_owner_response", + "decision_reason": "pending_owner_response", + "formal_release_lane_ready": false, + "gitea_push_authorized": false, + "not_approval": true, + "outcome_lanes": [ + "waiting_owner_response", + "quarantine_secret_or_raw_payload", + "reject_execution_request", + "request_supplement", + "ready_for_release_reviewer_validation", + "formal_gitea_merge_candidate", + "formal_patch_apply_candidate", + "safe_credential_push_candidate", + "waiting_production_readback", + "waiting_runtime_gate" + ], + "owner_response_accepted": false, + "owner_response_quarantined": false, + "owner_response_received": false, + "owner_response_rejected": false, + "owner_role_or_team": "pending_owner_response", + "patch_apply_authorized": false, + "post_deploy_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "production_deploy_authorized": false, + "production_readback_passed": false, + "redacted_evidence_refs": [], + "release_method": "pending_owner_response", + "request_id": "iwooos_wazuh_readonly_release_owner_request", + "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" + ], + "reviewer_checks": [ + "owner_identity_present", + "release_method_allowed", + "target_scope_matches_wazuh_branch_or_patch_set", + "all_ack_flags_true", + "all_evidence_fields_present", + "redacted_refs_only", + "secret_value_absent", + "no_plaintext_token_workaround", + "no_force_push", + "no_runtime_workaround", + "post_deploy_readback_required", + "rollback_owner_present", + "live_metadata_gate_separate", + "active_response_stays_false", + "counts_transition_safe" + ], + "rollback_owner": "pending_owner_response", + "runtime_gate": false, + "status": "waiting_owner_response", + "supplement_requested": false, + "target_branch_or_patch_set": "pending_owner_response" + }, + "execution_boundaries": { + "force_push_allowed": false, + "gitea_push_authorized": false, + "host_write_authorized": false, + "kali_active_scan_authorized": false, + "not_authorization": true, + "patch_apply_authorized": false, + "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:32:00+08:00", + "mode": "metadata_only_acceptance_no_secret_no_runtime_no_push", + "reviewer_instructions": [ + "只有具備完整欄位、脫敏 evidence refs、無 secret、無 runtime 要求的 owner response 才能進 reviewer validation。", + "一般批准繼續、截圖、口頭同意或未列 release method 的訊息都不能增加 accepted count。", + "即使 owner response accepted,也只代表可進正式 release lane;Wazuh live metadata 與 active response 仍是不同 gate。", + "production readback 必須在部署後不加 --allow-predeploy-404 執行,且不得回 404。" + ], + "schema_version": "iwooos_wazuh_readonly_release_owner_response_acceptance_v1", + "status": "waiting_owner_response", + "summary": { + "acceptance_candidate_count": 1, + "accepted_ack_flag_count": 0, + "accepted_evidence_field_count": 0, + "blocked_action_count": 13, + "formal_release_lane_ready_count": 0, + "gitea_push_authorized_count": 0, + "outcome_lane_count": 10, + "owner_response_accepted_count": 0, + "owner_response_quarantined_count": 0, + "owner_response_received_count": 0, + "owner_response_rejected_count": 0, + "patch_apply_authorized_count": 0, + "production_deploy_authorized_count": 0, + "production_readback_passed_count": 0, + "required_ack_flag_count": 6, + "required_evidence_field_count": 6, + "reviewer_check_count": 15, + "runtime_gate_count": 0, + "supplement_requested_count": 0 + } +} diff --git a/scripts/security/security-mirror-progress-guard.py b/scripts/security/security-mirror-progress-guard.py index 49524935..9e8184d5 100755 --- a/scripts/security/security-mirror-progress-guard.py +++ b/scripts/security/security-mirror-progress-guard.py @@ -99,6 +99,14 @@ def validate(root: Path) -> None: str(root / "scripts" / "security" / "wazuh-readonly-release-lane-preflight.py") ) wazuh_readonly_release_lane_preflight["validate"](root) + wazuh_readonly_release_owner_request = runpy.run_path( + str(root / "scripts" / "security" / "wazuh-readonly-release-owner-request.py") + ) + wazuh_readonly_release_owner_request["validate"](root) + wazuh_readonly_release_owner_response_acceptance = runpy.run_path( + str(root / "scripts" / "security" / "wazuh-readonly-release-owner-response-acceptance.py") + ) + wazuh_readonly_release_owner_response_acceptance["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-owner-request.py b/scripts/security/wazuh-readonly-release-owner-request.py new file mode 100644 index 00000000..4ebe2f83 --- /dev/null +++ b/scripts/security/wazuh-readonly-release-owner-request.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +""" +IwoooS Wazuh 只讀 API release owner request 草稿。 + +本工具只產生 / 驗證 repo 內 committed snapshot;不送 request、不讀 +credential、不推送、不部署、不查 Wazuh、不改 runtime。 +""" + +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-owner-request.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", +] + +FORBIDDEN_PAYLOADS = [ + "token", + "secret", + "private_key", + "cookie", + "session", + "authorization_header", + "runner_token", + "webhook_secret", + "wazuh_password", + "wazuh_raw_payload", + "git_credential", + "repo_archive", +] + +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", +] + +HANDOFF_ENVELOPE_FIELDS = [ + "request_id", + "stage_id", + "recipient_role_or_team", + "sender_role_or_team", + "requested_response_window", + "allowed_release_methods", + "required_ack_flags", + "required_evidence_fields", + "target_branch_or_patch_set", + "post_deploy_readback_command", + "forbidden_payloads", + "blocked_runtime_actions", + "followup_owner", + "not_approval", +] + + +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_owner_request_v1", + "generated_at": generated_at or now_iso(), + "status": "draft_not_dispatched_waiting_release_lane_owner", + "mode": "repo_request_draft_no_secret_no_runtime_no_push", + "summary": { + "request_draft_count": 1, + "required_ack_flag_count": len(REQUIRED_ACK_FLAGS), + "required_evidence_field_count": len(REQUIRED_EVIDENCE_FIELDS), + "allowed_release_method_count": len(ALLOWED_RELEASE_METHODS), + "forbidden_payload_count": len(FORBIDDEN_PAYLOADS), + "blocked_action_count": len(BLOCKED_ACTIONS), + "handoff_envelope_field_count": len(HANDOFF_ENVELOPE_FIELDS), + "request_sent_count": 0, + "recipient_confirmed_count": 0, + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "formal_release_lane_ready_count": 0, + "gitea_push_authorized_count": 0, + "patch_apply_authorized_count": 0, + "production_deploy_authorized_count": 0, + "production_readback_passed_count": 0, + "runtime_gate_count": 0, + }, + "request_draft": { + "request_id": "iwooos_wazuh_readonly_release_owner_request", + "stage_id": "P0-IWOOOS-WAZUH-RELEASE", + "recipient_role_or_team": "pending_release_lane_owner", + "sender_role_or_team": "iwooos_security_reviewer", + "requested_response_window": "not_scheduled", + "allowed_release_methods": ALLOWED_RELEASE_METHODS, + "required_ack_flags": REQUIRED_ACK_FLAGS, + "required_evidence_fields": REQUIRED_EVIDENCE_FIELDS, + "target_branch": "codex/iwooos-wazuh-boundary-guard-20260624", + "target_branch_readback": "git log --oneline gitea/main..HEAD", + "target_patch_set_readback": "git format-patch gitea/main..HEAD after final docs commit; record sha256 outside committed docs", + "post_deploy_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "redacted_evidence_refs": [ + "docs/security/IWOOOS-WAZUH-READONLY-API-RELEASE-HANDOFF.md", + "docs/security/wazuh-readonly-release-gate.snapshot.json", + "docs/security/wazuh-readonly-release-lane-preflight.snapshot.json", + ], + "forbidden_payloads": FORBIDDEN_PAYLOADS, + "blocked_runtime_actions": BLOCKED_ACTIONS, + "followup_owner": "pending_followup_owner", + "not_approval": True, + "request_sent": False, + "recipient_confirmed": False, + "owner_response_received": False, + "owner_response_accepted": False, + "runtime_gate": False, + "action_buttons_allowed": False, + }, + "execution_boundaries": { + "dispatch_authorized": False, + "request_sent": False, + "recipient_confirmed": False, + "repo_write_authorized": False, + "gitea_push_authorized": False, + "patch_apply_authorized": False, + "production_deploy_authorized": False, + "runtime_execution_authorized": False, + "secret_value_collection_allowed": False, + "plain_text_token_workaround_allowed": False, + "force_push_allowed": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "host_write_authorized": False, + "kali_active_scan_authorized": False, + "not_authorization": True, + }, + "handoff_envelope_fields": HANDOFF_ENVELOPE_FIELDS, + "send_after_conditions": [ + "先確認 gitea/main、Wazuh 分支與另一個 AwoooP Session 基線。", + "只送脫敏欄位與 refs;不得附 secret、raw Wazuh payload、git credential 或 runtime 操作要求。", + "一般批准繼續不是 release owner response。", + "收到 response 後仍需先通過 owner response acceptance ledger,不能直接 push 或 deploy。", + ], + } + + +def validate(root: Path) -> None: + snapshot_path = root / SNAPSHOT_PATH + if not snapshot_path.exists(): + raise SystemExit(f"BLOCKED Wazuh release owner request snapshot missing: {SNAPSHOT_PATH}") + snapshot = json.loads(snapshot_path.read_text(encoding="utf-8")) + expected = build_report(snapshot.get("generated_at")) + + for key in ("schema_version", "status", "mode"): + if snapshot.get(key) != expected[key]: + raise SystemExit(f"BLOCKED Wazuh release owner request {key} 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 owner request 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 owner request not_authorization must be true") + elif value is not False: + raise SystemExit(f"BLOCKED Wazuh release owner request execution_boundaries.{key}: expected false") + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 API release owner request 草稿") + 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_OWNER_REQUEST_OK " + f"drafts={summary['request_draft_count']} " + f"sent={summary['request_sent_count']} " + f"accepted={summary['owner_response_accepted_count']} " + f"runtime_gate={summary['runtime_gate_count']}" + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/security/wazuh-readonly-release-owner-response-acceptance.py b/scripts/security/wazuh-readonly-release-owner-response-acceptance.py new file mode 100644 index 00000000..35108110 --- /dev/null +++ b/scripts/security/wazuh-readonly-release-owner-response-acceptance.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 +""" +IwoooS Wazuh 只讀 API release owner response acceptance 帳本。 + +本工具定義未來 owner response 如何被收件、補件、隔離、拒收或進入 +reviewer validation;它不讀 credential、不推送、不部署、不查 Wazuh、 +不寫 runtime,也不把一般批准繼續當 release 授權。 +""" + +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-owner-response-acceptance.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", +] + +REVIEWER_CHECKS = [ + "owner_identity_present", + "release_method_allowed", + "target_scope_matches_wazuh_branch_or_patch_set", + "all_ack_flags_true", + "all_evidence_fields_present", + "redacted_refs_only", + "secret_value_absent", + "no_plaintext_token_workaround", + "no_force_push", + "no_runtime_workaround", + "post_deploy_readback_required", + "rollback_owner_present", + "live_metadata_gate_separate", + "active_response_stays_false", + "counts_transition_safe", +] + +OUTCOME_LANES = [ + "waiting_owner_response", + "quarantine_secret_or_raw_payload", + "reject_execution_request", + "request_supplement", + "ready_for_release_reviewer_validation", + "formal_gitea_merge_candidate", + "formal_patch_apply_candidate", + "safe_credential_push_candidate", + "waiting_production_readback", + "waiting_runtime_gate", +] + +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", + "mark_general_approval_as_release_response", + "mark_predeploy_404_as_passed_readback", +] + + +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_owner_response_acceptance_v1", + "generated_at": generated_at or now_iso(), + "status": "waiting_owner_response", + "mode": "metadata_only_acceptance_no_secret_no_runtime_no_push", + "summary": { + "acceptance_candidate_count": 1, + "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, + "reviewer_check_count": len(REVIEWER_CHECKS), + "outcome_lane_count": len(OUTCOME_LANES), + "blocked_action_count": len(BLOCKED_ACTIONS), + "owner_response_received_count": 0, + "owner_response_accepted_count": 0, + "owner_response_rejected_count": 0, + "owner_response_quarantined_count": 0, + "supplement_requested_count": 0, + "formal_release_lane_ready_count": 0, + "gitea_push_authorized_count": 0, + "patch_apply_authorized_count": 0, + "production_deploy_authorized_count": 0, + "production_readback_passed_count": 0, + "runtime_gate_count": 0, + }, + "acceptance_candidate": { + "acceptance_candidate_id": "iwooos_wazuh_readonly_release_owner_response", + "request_id": "iwooos_wazuh_readonly_release_owner_request", + "status": "waiting_owner_response", + "owner_role_or_team": "pending_owner_response", + "decision": "pending_owner_response", + "decision_reason": "pending_owner_response", + "release_method": "pending_owner_response", + "target_branch_or_patch_set": "pending_owner_response", + "post_deploy_readback_command": "python3 scripts/security/wazuh-readonly-production-readback.py --json", + "rollback_owner": "pending_owner_response", + "redacted_evidence_refs": [], + "ack_flags": {flag: False for flag in REQUIRED_ACK_FLAGS}, + "required_ack_flags": REQUIRED_ACK_FLAGS, + "required_evidence_fields": REQUIRED_EVIDENCE_FIELDS, + "reviewer_checks": REVIEWER_CHECKS, + "outcome_lanes": OUTCOME_LANES, + "blocked_actions": BLOCKED_ACTIONS, + "not_approval": True, + "owner_response_received": False, + "owner_response_accepted": False, + "owner_response_rejected": False, + "owner_response_quarantined": False, + "supplement_requested": False, + "formal_release_lane_ready": False, + "gitea_push_authorized": False, + "patch_apply_authorized": False, + "production_deploy_authorized": False, + "production_readback_passed": False, + "runtime_gate": False, + }, + "execution_boundaries": { + "repo_write_authorized": False, + "gitea_push_authorized": False, + "patch_apply_authorized": False, + "production_deploy_authorized": False, + "runtime_execution_authorized": False, + "secret_value_collection_allowed": False, + "plain_text_token_workaround_allowed": False, + "force_push_allowed": False, + "wazuh_api_live_query_authorized": False, + "wazuh_active_response_authorized": False, + "host_write_authorized": False, + "kali_active_scan_authorized": False, + "not_authorization": True, + }, + "reviewer_instructions": [ + "只有具備完整欄位、脫敏 evidence refs、無 secret、無 runtime 要求的 owner response 才能進 reviewer validation。", + "一般批准繼續、截圖、口頭同意或未列 release method 的訊息都不能增加 accepted count。", + "即使 owner response accepted,也只代表可進正式 release lane;Wazuh live metadata 與 active response 仍是不同 gate。", + "production readback 必須在部署後不加 --allow-predeploy-404 執行,且不得回 404。", + ], + } + + +def validate(root: Path) -> None: + snapshot_path = root / SNAPSHOT_PATH + if not snapshot_path.exists(): + raise SystemExit( + f"BLOCKED Wazuh release owner response acceptance snapshot missing: {SNAPSHOT_PATH}" + ) + snapshot = json.loads(snapshot_path.read_text(encoding="utf-8")) + expected = build_report(snapshot.get("generated_at")) + + for key in ("schema_version", "status", "mode"): + if snapshot.get(key) != expected[key]: + raise SystemExit(f"BLOCKED Wazuh release owner response acceptance {key} 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 owner response acceptance 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 owner response acceptance not_authorization must be true" + ) + elif value is not False: + raise SystemExit( + f"BLOCKED Wazuh release owner response acceptance execution_boundaries.{key}: expected false" + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description="IwoooS Wazuh 只讀 API release owner response acceptance") + 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_OWNER_RESPONSE_ACCEPTANCE_OK " + f"received={summary['owner_response_received_count']} " + f"accepted={summary['owner_response_accepted_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())