Compare commits

..

8 Commits

Author SHA1 Message Date
Your Name
1e359886af feat(iwooos): add runtime security readback board 2026-06-26 17:48:48 +08:00
Your Name
88630ab7fa fix(web): keep mobile navigation readable
Some checks failed
Code Review / ai-code-review (push) Successful in 15s
CD Pipeline / tests (push) Successful in 1m44s
CD Pipeline / build-and-deploy (push) Successful in 4m54s
CD Pipeline / post-deploy-checks (push) Has been cancelled
2026-06-26 17:44:54 +08:00
AWOOOI CD
4ad579a09c chore(cd): deploy 342bb23 [skip ci] 2026-06-26 09:41:03 +00:00
Your Name
342bb23cf1 fix(web): restore operator navigation IA
Some checks failed
Code Review / ai-code-review (push) Successful in 14s
CD Pipeline / tests (push) Successful in 1m47s
CD Pipeline / build-and-deploy (push) Successful in 5m14s
CD Pipeline / post-deploy-checks (push) Has been cancelled
2026-06-26 17:33:39 +08:00
Your Name
35ab800ff7 chore(cd): trigger latest formal version redeploy 2026-06-26 14:29:39 +08:00
Your Name
03e5557f91 feat(web): consolidate navigation IA shell
Some checks failed
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
2026-06-26 14:17:00 +08:00
Your Name
84791ab5d4 chore(cd): trigger latest main redeploy 2026-06-26 14:03:11 +08:00
ogt
ec8377e732 ops(reboot): add post-reboot owner response preflight
Some checks failed
Code Review / ai-code-review (push) Successful in 14s
AI 技術雷達監控 / ai-technology-watch (push) Successful in 38s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
2026-06-26 13:30:41 +08:00
20 changed files with 1557 additions and 174 deletions

View File

@@ -7,15 +7,22 @@ Wazuh 接線採用只讀 metadata 模式:預設關閉、不保存 raw payload
from __future__ import annotations
import asyncio
import json
import os
from base64 import b64encode
from typing import Any
from urllib.parse import urljoin, urlparse
import httpx
from fastapi import APIRouter
from fastapi import APIRouter, HTTPException, status
from fastapi.responses import JSONResponse
from src.services.iwooos_runtime_security_readback import (
load_latest_iwooos_runtime_security_readback,
)
from src.services.public_redaction import redact_public_lan_topology
router = APIRouter(tags=["IwoooS Security"])
REQUEST_TIMEOUT_SECONDS = 5.0
@@ -198,3 +205,30 @@ async def get_iwooos_wazuh_readonly_status_compat() -> JSONResponse:
@router.get("/api/v1/iwooos/wazuh")
async def get_iwooos_wazuh_readonly_status_v1() -> JSONResponse:
return await _wazuh_readonly_status()
@router.get(
"/api/v1/iwooos/runtime-security-readback",
response_model=dict[str, Any],
summary="取得 IwoooS runtime security readback",
description=(
"讀取最新已提交的 IwoooS 資安只讀快照,彙總 Wazuh、Kali、SOC/SIEM、"
"告警可讀性、owner dispatch 與外部入侵防護 Gate。此端點不呼叫 Wazuh / Kali / "
"主機 / Docker / Nginx / firewall / Telegram不收集 secret不授權 runtime 寫入。"
),
)
async def get_iwooos_runtime_security_readback() -> dict[str, Any]:
"""回傳 IwoooS 資安 runtime readback 只讀總板。"""
try:
payload = await asyncio.to_thread(load_latest_iwooos_runtime_security_readback)
return redact_public_lan_topology(payload)
except FileNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=str(exc),
) from exc
except (json.JSONDecodeError, ValueError) as exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"IwoooS runtime security readback 無效:{exc}",
) from exc

View File

@@ -0,0 +1,322 @@
"""
IwoooS runtime security readback.
Loads committed security snapshots and exposes a public-safe, read-only runtime
security board. This module never queries Wazuh, Kali, hosts, Docker, Nginx,
firewalls, databases, Telegram, or secrets.
"""
from __future__ import annotations
import json
from pathlib import Path
from typing import Any
from src.services.snapshot_paths import default_security_dir
_DEFAULT_SECURITY_DIR = default_security_dir(Path(__file__))
_SNAPSHOT_FILES = {
"owner_gap": "s4-9-owner-response-gap-audit.snapshot.json",
"wazuh_coverage": "wazuh-managed-host-coverage-gate.snapshot.json",
"wazuh_runtime": "wazuh-agent-visibility-runtime-gate.snapshot.json",
"kali_status": "kali-integration-status.snapshot.json",
"soc_control": "soc-siem-kali-wazuh-integration-control.snapshot.json",
"alert_readability": "telegram-alert-readability-guard.snapshot.json",
"owner_dispatch": "monitoring-owner-request-draft.snapshot.json",
"intrusion_prevention": "external-host-intrusion-prevention-control.snapshot.json",
}
_EXPECTED_SCHEMAS = {
"owner_gap": "s4_9_owner_response_gap_audit_v1",
"wazuh_coverage": "wazuh_managed_host_coverage_gate_v1",
"wazuh_runtime": "wazuh_agent_visibility_runtime_gate_v1",
"kali_status": "kali_integration_status_v1",
"soc_control": "soc_siem_kali_wazuh_integration_control_v1",
"alert_readability": "telegram_alert_readability_guard_v1",
"owner_dispatch": "monitoring_owner_request_draft_v1",
"intrusion_prevention": "external_host_intrusion_prevention_control_v1",
}
_FALSE_BOUNDARY_KEYS = {
"active_scan_authorized",
"alertmanager_reload_authorized",
"auto_block_authorized",
"credentialed_scan_authorized",
"firewall_change_authorized",
"host_write_authorized",
"kali_execute_authorized",
"kali_scan_authorized",
"nginx_reload_authorized",
"production_write_authorized",
"runtime_execution_authorized",
"runtime_gate_open",
"secret_value_collection_allowed",
"telegram_send_authorized",
"wazuh_active_response_authorized",
"wazuh_api_live_query_authorized",
}
def load_latest_iwooos_runtime_security_readback(
security_dir: Path | None = None,
) -> dict[str, Any]:
"""Load and normalize the current IwoooS runtime security readback."""
directory = security_dir or _DEFAULT_SECURITY_DIR
snapshots = {key: _load_snapshot(directory, key, filename) for key, filename in _SNAPSHOT_FILES.items()}
_require_runtime_boundaries(snapshots)
owner_gap_summary = _summary(snapshots["owner_gap"])
wazuh_summary = _summary(snapshots["wazuh_coverage"])
soc_summary = _summary(snapshots["soc_control"])
alert_summary = _summary(snapshots["alert_readability"])
dispatch_summary = _summary(snapshots["owner_dispatch"])
intrusion_summary = _summary(snapshots["intrusion_prevention"])
source_refs = [f"docs/security/{filename}" for filename in _SNAPSHOT_FILES.values()]
runtime_gate_count = _max_summary_count(
snapshots,
"runtime_gate_count",
"active_response_authorized_count",
"kali_active_scan_authorized_count",
"telegram_send_authorized_count",
)
return {
"schema_version": "iwooos_runtime_security_readback_v1",
"status": "blocked_waiting_owner_evidence_and_runtime_gates",
"mode": "committed_snapshot_readback_only_no_runtime_query",
"source_refs": source_refs,
"summary": {
"source_snapshot_count": len(source_refs),
"p0_lane_count": 6,
"control_plane_visibility_percent": _average_percent(
soc_summary.get("coverage_percent_after_soc_integration_control"),
intrusion_summary.get("coverage_percent_after_prevention_control"),
_alert_contract_percent(alert_summary),
),
"actual_runtime_acceptance_percent": 0,
"owner_response_received_count": _int(owner_gap_summary.get("owner_response_received_count")),
"owner_response_accepted_count": _int(owner_gap_summary.get("owner_response_accepted_count")),
"redacted_evidence_refs_received_count": 0,
"request_sent_count": _int(dispatch_summary.get("request_sent_count")),
"wazuh_expected_host_scope_count": _int(wazuh_summary.get("expected_host_scope_count")),
"wazuh_manager_registry_accepted_count": _int(wazuh_summary.get("manager_registry_accepted_count")),
"wazuh_transport_observed_count": _int(wazuh_summary.get("manager_transport_established_connection_count")),
"wazuh_dashboard_api_degraded_observed_count": _int(
wazuh_summary.get("dashboard_api_degraded_observed_count")
),
"kali_active_scan_authorized_count": _int(soc_summary.get("kali_active_scan_authorized_count")),
"kali_execute_authorized_count": _int(soc_summary.get("kali_execute_authorized_count")),
"kali_finding_envelope_accepted_count": _int(soc_summary.get("kali_finding_envelope_accepted_count")),
"alert_formatter_contract_marker_count": _int(alert_summary.get("source_formatter_marker_count")),
"alert_receipt_runtime_send_count": _int(alert_summary.get("telegram_send_authorized_count")),
"intrusion_prevention_candidate_count": _int(intrusion_summary.get("urgent_prevention_candidate_count")),
"runtime_gate_count": runtime_gate_count,
},
"lanes": [
_lane(
"wazuh_registry",
"blocked_waiting_manager_registry",
0,
"locked",
"manager_registry_cross_check",
{
"expected_hosts": wazuh_summary.get("expected_host_scope_count", 0),
"transport_observed": wazuh_summary.get("manager_transport_established_connection_count", 0),
"registry_accepted": wazuh_summary.get("manager_registry_accepted_count", 0),
},
["docs/security/wazuh-managed-host-coverage-gate.snapshot.json"],
),
_lane(
"wazuh_dashboard_api",
"degraded_api_connection_not_green",
0,
"warn",
"dashboard_api_rbac_tls_repair_readback",
{
"dashboard_api_degraded": wazuh_summary.get("dashboard_api_degraded_observed_count", 0),
"runtime_gate": wazuh_summary.get("runtime_gate_count", 0),
"accepted_evidence": _accepted_evidence_count(snapshots["wazuh_runtime"]),
},
[
"docs/security/wazuh-managed-host-coverage-gate.snapshot.json",
"docs/security/wazuh-agent-visibility-runtime-gate.snapshot.json",
],
),
_lane(
"kali_intake",
snapshots["kali_status"].get("status", "blocked_waiting_kali_scope"),
0,
"locked",
"kali_scope_and_finding_envelope_accepted",
{
"active_scan_authorized": soc_summary.get("kali_active_scan_authorized_count", 0),
"execute_authorized": soc_summary.get("kali_execute_authorized_count", 0),
"finding_envelope_accepted": soc_summary.get("kali_finding_envelope_accepted_count", 0),
},
[
"docs/security/kali-integration-status.snapshot.json",
"docs/security/soc-siem-kali-wazuh-integration-control.snapshot.json",
],
),
_lane(
"alert_readability",
"contract_ready_no_send_receipt",
_alert_contract_percent(alert_summary),
"warn",
"alert_route_receipt_available",
{
"formatter_markers": alert_summary.get("source_formatter_marker_count", 0),
"required_markers": alert_summary.get("required_output_marker_count", 0),
"telegram_send": alert_summary.get("telegram_send_authorized_count", 0),
},
["docs/security/telegram-alert-readability-guard.snapshot.json"],
),
_lane(
"owner_dispatch",
snapshots["owner_dispatch"].get("status", "owner_request_draft_ready_not_dispatched"),
0,
"locked",
"owner_response_packet_delivery",
{
"request_drafts": dispatch_summary.get("request_draft_count", 0),
"request_sent": dispatch_summary.get("request_sent_count", 0),
"owner_accepted": dispatch_summary.get("owner_response_accepted_count", 0),
},
["docs/security/monitoring-owner-request-draft.snapshot.json"],
),
_lane(
"intrusion_prevention",
"candidate_only_no_runtime_containment",
_int(intrusion_summary.get("coverage_percent_after_prevention_control")),
"warn",
"redacted_evidence_refs_and_maintenance_window",
{
"urgent_candidates": intrusion_summary.get("urgent_prevention_candidate_count", 0),
"evidence_received": intrusion_summary.get("evidence_ref_received_count", 0),
"containment_accepted": intrusion_summary.get("containment_decision_accepted_count", 0),
},
["docs/security/external-host-intrusion-prevention-control.snapshot.json"],
),
],
"boundaries": {
"active_response_authorized": False,
"active_scan_authorized": False,
"action_buttons_allowed": False,
"host_write_authorized": False,
"kali_execute_authorized": False,
"nginx_reload_authorized": False,
"raw_payload_storage_allowed": False,
"runtime_execution_authorized": False,
"secret_value_collection_allowed": False,
"telegram_send_authorized": False,
"wazuh_active_response_authorized": False,
"wazuh_api_live_query_authorized": False,
"workflow_modification_authorized": False,
"not_authorization": True,
},
"no_false_green_rules": [
"dashboard_route_200_is_not_wazuh_registry_recovery",
"transport_count_is_not_full_host_management",
"ui_visible_is_not_runtime_authorization",
"owner_request_draft_is_not_owner_acceptance",
"kali_health_is_not_active_scan_authorization",
"alert_format_contract_is_not_telegram_send_receipt",
],
}
def _load_snapshot(directory: Path, key: str, filename: str) -> dict[str, Any]:
path = directory / filename
if not path.is_file():
raise FileNotFoundError(f"{path}: security snapshot not found")
with path.open(encoding="utf-8") as handle:
payload = json.load(handle)
if not isinstance(payload, dict):
raise ValueError(f"{path}: expected JSON object")
expected_schema = _EXPECTED_SCHEMAS[key]
if payload.get("schema_version") != expected_schema:
raise ValueError(f"{path}: expected schema_version={expected_schema}")
return payload
def _summary(payload: dict[str, Any]) -> dict[str, Any]:
summary = payload.get("summary")
return summary if isinstance(summary, dict) else {}
def _int(value: Any) -> int:
return value if isinstance(value, int) else 0
def _average_percent(*values: Any) -> int:
percents = [_int(value) for value in values if isinstance(value, int)]
return int(round(sum(percents) / len(percents))) if percents else 0
def _alert_contract_percent(summary: dict[str, Any]) -> int:
source_markers = _int(summary.get("source_formatter_marker_count"))
required_markers = max(1, _int(summary.get("required_output_marker_count")))
return min(100, int(round((source_markers / required_markers) * 100)))
def _accepted_evidence_count(payload: dict[str, Any]) -> int:
evidence = payload.get("required_evidence_before_green")
if not isinstance(evidence, list):
return 0
return sum(1 for item in evidence if isinstance(item, dict) and item.get("accepted") is True)
def _lane(
lane_id: str,
status_text: Any,
completion_percent: int,
tone: str,
next_gate: str,
metrics: dict[str, Any],
source_refs: list[str],
) -> dict[str, Any]:
return {
"lane_id": lane_id,
"status": str(status_text),
"completion_percent": completion_percent,
"tone": tone,
"next_gate": next_gate,
"metrics": {key: _int(value) for key, value in metrics.items()},
"source_refs": source_refs,
}
def _max_summary_count(
snapshots: dict[str, dict[str, Any]],
*keys: str,
) -> int:
return max((_int(_summary(payload).get(key)) for payload in snapshots.values() for key in keys), default=0)
def _require_runtime_boundaries(snapshots: dict[str, dict[str, Any]]) -> None:
for name, payload in snapshots.items():
summary = _summary(payload)
if _int(summary.get("runtime_gate_count")) != 0:
raise ValueError(f"{name}: runtime_gate_count must remain 0")
for key in (
"owner_response_accepted_count",
"wazuh_active_response_enabled_count",
"active_response_enabled_count",
"active_scan_authorized_count",
"kali_active_scan_authorized_count",
"telegram_send_authorized_count",
"host_write_authorized_count",
"secret_value_collection_allowed_count",
):
if key in summary and _int(summary.get(key)) != 0:
raise ValueError(f"{name}: {key} must remain 0")
boundaries = payload.get("execution_boundaries")
if isinstance(boundaries, dict):
invalid = sorted(
key for key in _FALSE_BOUNDARY_KEYS if key in boundaries and boundaries.get(key) is not False
)
if invalid:
raise ValueError(f"{name}: execution boundaries must remain false: {invalid}")

View File

@@ -35,3 +35,8 @@ def default_evaluations_dir(anchor: Path) -> Path:
def default_operations_dir(anchor: Path) -> Path:
"""Resolve the default committed operations snapshot directory."""
return resolve_repo_root(anchor) / "docs" / "operations"
def default_security_dir(anchor: Path) -> Path:
"""Resolve the default committed security snapshot directory."""
return resolve_repo_root(anchor) / "docs" / "security"

View File

@@ -0,0 +1,67 @@
from __future__ import annotations
from fastapi import FastAPI
from fastapi.testclient import TestClient
from src.api.v1.iwooos import router
from src.services.iwooos_runtime_security_readback import (
load_latest_iwooos_runtime_security_readback,
)
def _client() -> TestClient:
app = FastAPI()
app.include_router(router)
return TestClient(app)
def test_iwooos_runtime_security_readback_preserves_zero_runtime_gates() -> None:
payload = load_latest_iwooos_runtime_security_readback()
assert payload["schema_version"] == "iwooos_runtime_security_readback_v1"
assert payload["status"] == "blocked_waiting_owner_evidence_and_runtime_gates"
assert payload["summary"]["source_snapshot_count"] == 8
assert payload["summary"]["p0_lane_count"] == 6
assert payload["summary"]["runtime_gate_count"] == 0
assert payload["summary"]["owner_response_received_count"] == 0
assert payload["summary"]["owner_response_accepted_count"] == 0
assert payload["summary"]["wazuh_manager_registry_accepted_count"] == 0
assert payload["summary"]["kali_active_scan_authorized_count"] == 0
assert payload["summary"]["kali_execute_authorized_count"] == 0
assert payload["summary"]["alert_receipt_runtime_send_count"] == 0
assert payload["boundaries"]["runtime_execution_authorized"] is False
assert payload["boundaries"]["active_scan_authorized"] is False
assert payload["boundaries"]["wazuh_active_response_authorized"] is False
assert payload["boundaries"]["telegram_send_authorized"] is False
def test_iwooos_runtime_security_readback_lanes_are_candidate_only() -> None:
payload = load_latest_iwooos_runtime_security_readback()
lane_ids = {lane["lane_id"] for lane in payload["lanes"]}
assert lane_ids == {
"wazuh_registry",
"wazuh_dashboard_api",
"kali_intake",
"alert_readability",
"owner_dispatch",
"intrusion_prevention",
}
assert all(lane["metrics"] for lane in payload["lanes"])
assert all(lane["next_gate"] for lane in payload["lanes"])
assert all(lane["source_refs"] for lane in payload["lanes"])
assert any(lane["completion_percent"] > 0 for lane in payload["lanes"])
assert all(lane["lane_id"] != "wazuh_registry" or lane["completion_percent"] == 0 for lane in payload["lanes"])
def test_iwooos_runtime_security_readback_api_is_public_safe() -> None:
response = _client().get("/api/v1/iwooos/runtime-security-readback")
assert response.status_code == 200
data = response.json()
assert data["schema_version"] == "iwooos_runtime_security_readback_v1"
assert data["summary"]["runtime_gate_count"] == 0
assert data["boundaries"]["secret_value_collection_allowed"] is False
assert "192.168.0." not in response.text
assert "工作視窗" not in response.text
assert "批准!繼續" not in response.text

View File

@@ -72,6 +72,7 @@
"awooop": "AwoooP",
"awooopHome": "AwoooP 總覽",
"awooopWorkbench": "AwoooP 操作台",
"codeReview": "程式碼審查",
"knowledgeAutomation": "知識與自動化",
"governanceSecurity": "治理與安全",
"workItems": "工作鏈路",
@@ -20150,6 +20151,75 @@
}
}
},
"runtimeSecurityReadback": {
"eyebrow": "IwoooS Runtime 資安讀回",
"title": "六條 P0 資安線先接到同一張讀回板",
"subtitle": "這張板讀取後端彙整的只讀快照,集中顯示 Wazuh 清單驗收、儀表板 API、資安觀測節點 intake、告警可讀性、負責人送件與外部入侵防護它不查 live Wazuh、不啟動掃描、不送訊息、不改主機。",
"statusLabel": "讀回狀態",
"statusDetail": "讀回成功只代表 IwoooS 能看見目前的證據邊界runtime 寫入、主動回應、掃描、重啟、Nginx reload、workflow 修改與機密操作仍全部關閉。",
"laneStatusLabel": "目前狀態",
"laneNextGateLabel": "下一個 Gate",
"emptyLanes": "尚未讀回 P0 線,維持阻擋狀態。",
"boundaryTitle": "禁止誤判規則",
"boundaryIntro": "以下規則由後端 payload 回傳;若讀回失敗,前端只顯示保守 fallback。任何一條都不能被當成 runtime 授權。",
"status": {
"loading": "正在讀取只讀資安總板",
"failed": "只讀總板尚未部署或讀取失敗",
"readbackReady": "只讀總板已讀回,但 runtime 閘門仍關閉"
},
"summary": {
"controlPlane": {
"label": "控制面可視",
"detail": "框架、候選與證據線已接上,但不等於 runtime 完成。"
},
"runtimeAcceptance": {
"label": "執行驗收",
"detail": "實際 runtime acceptance 仍維持 0%。"
},
"wazuhRegistry": {
"label": "Wazuh 清單",
"detail": "管理器清單接受數仍為 0。"
},
"ownerAccepted": {
"label": "負責人驗收",
"detail": "收到 / 接受都必須由正式 owner response 證明。"
},
"kaliRuntime": {
"label": "Kali 執行",
"detail": "active scan / execute 都維持 0。"
},
"runtimeGate": {
"label": "Runtime Gate",
"detail": "任何修復、封鎖、重啟或 reload 都還沒開門。"
}
},
"lanes": {
"wazuh_registry": {
"title": "Wazuh manager registry",
"body": "必須拿到總數、在線、離線、從未連線與時間窗transport 或儀表板可見不能代替驗收。"
},
"wazuh_dashboard_api": {
"title": "Wazuh Dashboard API",
"body": "API connection / API version 還要補 readbackindex pattern 通過不能宣稱 Wazuh 全綠。"
},
"kali_intake": {
"title": "資安觀測節點 intake",
"body": "先收 health、scope、工具版本與 normalized finding envelopeactive scan 與 /execute 仍需獨立批准。"
},
"alert_readability": {
"title": "告警可讀性 receipt",
"body": "格式合約已能阻擋 raw output但 Telegram 實發、receipt 與修復結果仍未啟用。"
},
"owner_dispatch": {
"title": "負責人送件",
"body": "目前只有送件草案與欄位包request sent、received、accepted 全都不可假性上修。"
},
"intrusion_prevention": {
"title": "外部入侵防護",
"body": "候選已按公開入口、SSH、firewall、主機 runtime、runner、secret、Wazuh 等域分類;圍堵仍只能是候選。"
}
}
},
"wazuhLiveRouteReadback": {
"eyebrow": "Wazuh 正式路由只讀讀回",
"title": "正式站必須直接顯示 Wazuh 只讀路由狀態",

View File

@@ -72,6 +72,7 @@
"awooop": "AwoooP",
"awooopHome": "AwoooP 總覽",
"awooopWorkbench": "AwoooP 操作台",
"codeReview": "程式碼審查",
"knowledgeAutomation": "知識與自動化",
"governanceSecurity": "治理與安全",
"workItems": "工作鏈路",
@@ -20150,6 +20151,75 @@
}
}
},
"runtimeSecurityReadback": {
"eyebrow": "IwoooS Runtime 資安讀回",
"title": "六條 P0 資安線先接到同一張讀回板",
"subtitle": "這張板讀取後端彙整的只讀快照,集中顯示 Wazuh 清單驗收、儀表板 API、資安觀測節點 intake、告警可讀性、負責人送件與外部入侵防護它不查 live Wazuh、不啟動掃描、不送訊息、不改主機。",
"statusLabel": "讀回狀態",
"statusDetail": "讀回成功只代表 IwoooS 能看見目前的證據邊界runtime 寫入、主動回應、掃描、重啟、Nginx reload、workflow 修改與機密操作仍全部關閉。",
"laneStatusLabel": "目前狀態",
"laneNextGateLabel": "下一個 Gate",
"emptyLanes": "尚未讀回 P0 線,維持阻擋狀態。",
"boundaryTitle": "禁止誤判規則",
"boundaryIntro": "以下規則由後端 payload 回傳;若讀回失敗,前端只顯示保守 fallback。任何一條都不能被當成 runtime 授權。",
"status": {
"loading": "正在讀取只讀資安總板",
"failed": "只讀總板尚未部署或讀取失敗",
"readbackReady": "只讀總板已讀回,但 runtime 閘門仍關閉"
},
"summary": {
"controlPlane": {
"label": "控制面可視",
"detail": "框架、候選與證據線已接上,但不等於 runtime 完成。"
},
"runtimeAcceptance": {
"label": "執行驗收",
"detail": "實際 runtime acceptance 仍維持 0%。"
},
"wazuhRegistry": {
"label": "Wazuh 清單",
"detail": "管理器清單接受數仍為 0。"
},
"ownerAccepted": {
"label": "負責人驗收",
"detail": "收到 / 接受都必須由正式 owner response 證明。"
},
"kaliRuntime": {
"label": "Kali 執行",
"detail": "active scan / execute 都維持 0。"
},
"runtimeGate": {
"label": "Runtime Gate",
"detail": "任何修復、封鎖、重啟或 reload 都還沒開門。"
}
},
"lanes": {
"wazuh_registry": {
"title": "Wazuh manager registry",
"body": "必須拿到總數、在線、離線、從未連線與時間窗transport 或儀表板可見不能代替驗收。"
},
"wazuh_dashboard_api": {
"title": "Wazuh Dashboard API",
"body": "API connection / API version 還要補 readbackindex pattern 通過不能宣稱 Wazuh 全綠。"
},
"kali_intake": {
"title": "資安觀測節點 intake",
"body": "先收 health、scope、工具版本與 normalized finding envelopeactive scan 與 /execute 仍需獨立批准。"
},
"alert_readability": {
"title": "告警可讀性 receipt",
"body": "格式合約已能阻擋 raw output但 Telegram 實發、receipt 與修復結果仍未啟用。"
},
"owner_dispatch": {
"title": "負責人送件",
"body": "目前只有送件草案與欄位包request sent、received、accepted 全都不可假性上修。"
},
"intrusion_prevention": {
"title": "外部入侵防護",
"body": "候選已按公開入口、SSH、firewall、主機 runtime、runner、secret、Wazuh 等域分類;圍堵仍只能是候選。"
}
}
},
"wazuhLiveRouteReadback": {
"eyebrow": "Wazuh 正式路由只讀讀回",
"title": "正式站必須直接顯示 Wazuh 只讀路由狀態",

View File

@@ -8,7 +8,7 @@
"use client";
import { AppLayout } from "@/components/layout";
import { Link, usePathname } from "@/i18n/routing";
import { usePathname } from "@/i18n/routing";
import {
BrainCircuit,
CalendarDays,
@@ -116,29 +116,6 @@ export default function AwoooPLayout({
<span>{t("status.score")}</span>
</div>
<nav
aria-label={t("workflowRailLabel")}
className="flex max-w-full flex-wrap gap-1 overflow-hidden border-t border-[#e0ddd4] px-3 py-2 sm:px-5"
>
{navItems.map((item) => {
const active = activeItem.href === item.href;
return (
<Link
key={item.href}
href={item.href}
aria-current={active ? "page" : undefined}
className={[
"shrink-0 rounded-button border px-3 py-1.5 text-xs font-semibold transition-colors",
active
? "border-[#d97757] bg-[#fff7ed] text-[#141413]"
: "border-[#d8d3c7] bg-white text-[#6f6b62] hover:border-[#c8c1b3] hover:text-[#141413]",
].join(" ")}
>
{t(`nav.${item.labelKey}`)}
</Link>
);
})}
</nav>
</div>
<div className="w-full max-w-full">

View File

@@ -33,6 +33,7 @@ import Link from 'next/link'
import { useTranslations } from 'next-intl'
import { useEffect, useRef, useState, type ReactNode } from 'react'
import { AppLayout } from '@/components/layout'
import { apiClient, type IwoooSRuntimeSecurityReadbackResponse } from '@/lib/api-client'
type PostureMetric = {
key: string
@@ -319,6 +320,13 @@ type WazuhReadonlyStatusResponse = {
}
}
type RuntimeSecurityReadbackSummaryItem = {
key: 'controlPlane' | 'runtimeAcceptance' | 'wazuhRegistry' | 'ownerAccepted' | 'kaliRuntime' | 'runtimeGate'
value: string
icon: typeof ShieldCheck
tone: 'steady' | 'warn' | 'locked'
}
type SocSiemKaliWazuhIntegrationItem = {
key: string
check: string
@@ -1143,6 +1151,15 @@ const progressIntegrityRibbonBoundaries = [
'not_authorization=true',
]
const runtimeSecurityReadbackFallbackRules = [
'dashboard_route_200_is_not_wazuh_registry_recovery',
'transport_count_is_not_full_host_management',
'ui_visible_is_not_runtime_authorization',
'owner_request_draft_is_not_owner_acceptance',
'kali_health_is_not_active_scan_authorization',
'alert_format_contract_is_not_telegram_send_receipt',
]
const firstScreenDepthLayers: FirstScreenDepthLayer[] = [
{ key: 'visible', value: '4', icon: ShieldCheck, tone: 'steady' },
{ key: 'advanced', value: '2', icon: SearchCheck, tone: 'warn', href: '#iwooos-decision-gate-visuals' },
@@ -8037,6 +8054,233 @@ function IwoooSWazuhIntrusionReadbackBoard() {
)
}
function IwoooSRuntimeSecurityReadbackBoard() {
const t = useTranslations('iwooos.runtimeSecurityReadback')
const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const }
const [data, setData] = useState<IwoooSRuntimeSecurityReadbackResponse | null>(null)
const [loading, setLoading] = useState(true)
const [failed, setFailed] = useState(false)
useEffect(() => {
let mounted = true
async function loadReadback() {
setLoading(true)
setFailed(false)
try {
const payload = await apiClient.getIwoooSRuntimeSecurityReadback()
if (mounted) {
setData(payload)
}
} catch {
if (mounted) {
setData(null)
setFailed(true)
}
} finally {
if (mounted) {
setLoading(false)
}
}
}
loadReadback()
return () => {
mounted = false
}
}, [])
const summary = data?.summary
const statusKey = loading ? 'loading' : failed || !data ? 'failed' : 'readbackReady'
const statusTone: 'steady' | 'warn' | 'locked' = loading ? 'warn' : failed || !data ? 'warn' : 'locked'
const summaryItems: RuntimeSecurityReadbackSummaryItem[] = [
{
key: 'controlPlane',
value: summary ? `${summary.control_plane_visibility_percent}%` : '...',
icon: ShieldCheck,
tone: summary && summary.control_plane_visibility_percent > 0 ? 'steady' : 'warn',
},
{
key: 'runtimeAcceptance',
value: summary ? `${summary.actual_runtime_acceptance_percent}%` : '...',
icon: Lock,
tone: 'locked',
},
{
key: 'wazuhRegistry',
value: summary ? String(summary.wazuh_manager_registry_accepted_count) : '...',
icon: Radar,
tone: 'locked',
},
{
key: 'ownerAccepted',
value: summary ? `${summary.owner_response_accepted_count}/${summary.owner_response_received_count}` : '...',
icon: ClipboardCheck,
tone: 'locked',
},
{
key: 'kaliRuntime',
value: summary ? `${summary.kali_active_scan_authorized_count}/${summary.kali_execute_authorized_count}` : '...',
icon: SearchCheck,
tone: 'locked',
},
{
key: 'runtimeGate',
value: summary ? String(summary.runtime_gate_count) : '...',
icon: Lock,
tone: 'locked',
},
]
const lanes = data?.lanes ?? []
return (
<section
style={{ marginBottom: 14, maxWidth: '100%', overflow: 'hidden' }}
data-testid="iwooos-runtime-security-readback-board"
>
<div style={{ ...band, padding: 16, background: '#fbfcfb', borderColor: '#c9ded2' }}>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 300px), 1fr))', gap: 14 }}>
<div style={{ minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#315f45', fontSize: 12, fontWeight: 700 }}>
<ShieldCheck size={17} color="#2f7d48" />
{t('eyebrow')}
</div>
<h2 style={{ fontSize: 17, margin: '8px 0 0', color: '#141413' }}>{t('title')}</h2>
<p style={{ fontSize: 12, color: '#405f4f', margin: '6px 0 0', lineHeight: 1.55, ...textWrap }}>
{t('subtitle')}
</p>
</div>
<div style={{ border: '0.5px solid #d3e4d9', borderRadius: 8, padding: 12, background: '#fff', minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
<span style={{ fontSize: 11, color: '#627466', fontWeight: 700 }}>{t('statusLabel')}</span>
<ToneDot tone={statusTone} />
</div>
<div style={{ marginTop: 8, fontSize: 15, color: toneColors[statusTone], fontWeight: 700, ...textWrap }}>
{t(`status.${statusKey}` as never)}
</div>
<p style={{ fontSize: 11, color: '#405f4f', lineHeight: 1.5, margin: '8px 0 0', ...textWrap }}>
{t('statusDetail')}
</p>
</div>
</div>
<div
style={{
marginTop: 14,
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 132px), 1fr))',
gap: 8,
}}
>
{summaryItems.map(item => {
const Icon = item.icon
return (
<div key={item.key} style={{ border: '0.5px solid #d3e4d9', borderRadius: 8, padding: 12, background: '#fff', minWidth: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<span style={{ fontSize: 11, color: '#627466', ...textWrap }}>{t(`summary.${item.key}.label` as never)}</span>
<Icon size={16} color={toneColors[item.tone]} />
</div>
<div style={{ fontSize: 20, fontWeight: 700, color: toneColors[item.tone], lineHeight: 1.1, marginTop: 8, ...textWrap }}>
{item.value}
</div>
<p style={{ fontSize: 11, color: '#405f4f', lineHeight: 1.45, margin: '8px 0 0', ...textWrap }}>
{t(`summary.${item.key}.detail` as never)}
</p>
</div>
)
})}
</div>
<div
style={{
marginTop: 14,
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 220px), 1fr))',
gap: 10,
}}
>
{lanes.length === 0 ? (
<div style={{ border: '0.5px solid #d3e4d9', borderRadius: 8, background: '#fff', padding: 13, color: '#405f4f', fontSize: 12 }}>
{t('emptyLanes')}
</div>
) : lanes.map(lane => (
<div
key={lane.lane_id}
style={{
border: `0.5px solid ${lane.tone === 'steady' ? '#bddfc9' : lane.tone === 'warn' ? '#e6c8b8' : '#dad7ce'}`,
borderRadius: 8,
background: lane.tone === 'warn' ? '#fffaf7' : lane.tone === 'locked' ? '#f8f7f3' : '#fff',
padding: 13,
display: 'grid',
gap: 10,
minWidth: 0,
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<div style={{ fontSize: 12, color: toneColors[lane.tone], fontWeight: 700, ...textWrap }}>
{t(`lanes.${lane.lane_id}.title` as never)}
</div>
<span style={{ fontSize: 12, color: toneColors[lane.tone], fontWeight: 700 }}>{lane.completion_percent}%</span>
</div>
<div style={{ fontSize: 11, color: '#405f4f', lineHeight: 1.45, ...textWrap }}>
{t(`lanes.${lane.lane_id}.body` as never)}
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 6 }}>
<div style={{ border: '0.5px solid #e1e8df', borderRadius: 8, background: '#fff', padding: 8, minWidth: 0 }}>
<div style={{ fontSize: 10, color: '#627466', fontWeight: 700 }}>{t('laneStatusLabel')}</div>
<div style={{ marginTop: 5, fontSize: 11, color: '#141413', fontWeight: 700, ...textWrap }}>{lane.status}</div>
</div>
<div style={{ border: '0.5px solid #e1e8df', borderRadius: 8, background: '#fff', padding: 8, minWidth: 0 }}>
<div style={{ fontSize: 10, color: '#627466', fontWeight: 700 }}>{t('laneNextGateLabel')}</div>
<div style={{ marginTop: 5, fontSize: 11, color: '#141413', fontWeight: 700, ...textWrap }}>{lane.next_gate}</div>
</div>
</div>
</div>
))}
</div>
<details
data-testid="iwooos-runtime-security-readback-boundaries"
style={{
marginTop: 12,
border: '0.5px solid #d3e4d9',
borderRadius: 8,
background: '#fff',
padding: '8px 10px',
}}
>
<summary style={{ cursor: 'pointer', fontSize: 12, fontWeight: 700, color: '#315f45' }}>
{t('boundaryTitle')}
</summary>
<p style={{ fontSize: 11, color: '#405f4f', lineHeight: 1.5, margin: '8px 0', ...textWrap }}>
{t('boundaryIntro')}
</p>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 6 }}>
{(data?.no_false_green_rules ?? runtimeSecurityReadbackFallbackRules).map(item => (
<code
key={item}
style={{
border: '0.5px solid #d3e4d9',
borderRadius: 8,
padding: '6px 8px',
color: '#405f4f',
fontSize: 11,
lineHeight: 1.4,
background: '#f7fbf8',
overflowWrap: 'anywhere',
}}
>
{item}
</code>
))}
</div>
</details>
</div>
</section>
)
}
function wazuhReadonlyStatusKey(httpStatus: number | null, data: WazuhReadonlyStatusResponse | null) {
if (httpStatus === 404) return 'predeploy'
if (!data) return 'unavailable'
@@ -21933,6 +22177,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) {
<IwoooSAgentBountySecurityOnboardingBoard />
<IwoooSRolloutRiskReadOnlyBoard />
<IwoooSWazuhIntrusionReadbackBoard />
<IwoooSRuntimeSecurityReadbackBoard />
<IwoooSWazuhLiveRouteReadbackBoard />
<IwoooSWazuhReleaseGateBoard />
<IwoooSWazuhOwnerEvidencePreflightBoard />

View File

@@ -68,7 +68,13 @@ export function AppLayout({
useEffect(() => {
const media = window.matchMedia(MOBILE_SHELL_MEDIA)
const updateMobileShell = () => setMobileShell(media.matches)
const updateMobileShell = () => {
const isMobile = media.matches
setMobileShell(isMobile)
if (isMobile) {
setCollapsed(false)
}
}
updateMobileShell()
media.addEventListener('change', updateMobileShell)
@@ -102,7 +108,10 @@ export function AppLayout({
// Keep the navigation shell in the server-rendered HTML. If a rolling deploy
// or stale browser cache delays hydration, the operator still has navigation.
const effectiveCollapsed = mounted ? collapsed : false
const shellCollapsed = mobileShell || effectiveCollapsed
const shellCollapsed = effectiveCollapsed
const contentOffsetClass = shellCollapsed
? mobileShell ? 'ml-[48px]' : 'ml-[64px]'
: 'ml-[224px]'
return (
<div className="min-h-screen bg-nothing-gray-50">
@@ -137,7 +146,7 @@ export function AppLayout({
'relative z-10',
'pt-[68px]',
'transition-all duration-300 ease-out',
mobileShell ? 'ml-[48px]' : shellCollapsed ? 'ml-[64px]' : 'ml-[224px]'
contentOffsetClass
)}
>
{fullBleed ? (

View File

@@ -51,7 +51,7 @@ export function Header({
window.location.href = newPath
}, [locale, pathname])
const brandWidth = compact ? 64 : sidebarCollapsed ? 64 : 224
const brandWidth = sidebarCollapsed ? 64 : 224
return (
<header

View File

@@ -26,14 +26,21 @@ import { usePathname } from 'next/navigation'
import Link from 'next/link'
import { cn } from '@/lib/utils'
import {
BarChart3,
Bell,
BookOpen,
BrainCircuit,
Building2,
ChevronLeft,
ChevronRight,
ClipboardList,
FileText,
GitPullRequest,
HelpCircle,
LayoutDashboard,
Monitor,
Package,
PlayCircle,
Radar,
Settings,
ShieldCheck,
@@ -71,10 +78,10 @@ type NavSection = {
}
// ============================================================
// 2026-06-04: Operator-first IA
// 2026-06-26: Operator-first IA repair
// ============================================================
// 參考 Material / Atlassian / Carbon 的 app shell 模式:
// 側欄放高頻任務與產品導航,Header 留給搜尋與全域工具
// 側欄放高頻任務與產品導航,頁面內不再重複用二層分頁藏入口
// ============================================================
const NAV_SECTIONS: NavSection[] = [
@@ -83,22 +90,54 @@ const NAV_SECTIONS: NavSection[] = [
items: [
{ id: 'command-center', href: '/', labelKey: 'commandCenter', Icon: LayoutDashboard },
{
id: 'awooop-workbench',
id: 'awooop-overview',
href: '/awooop',
labelKey: 'awooopWorkbench',
labelKey: 'awooopHome',
Icon: BrainCircuit,
exact: true,
badge: true,
aliases: [
'/awooop/work-items',
'/awooop/runs',
'/awooop/approvals',
'/awooop/contracts',
'/awooop/tenants',
'/alerts',
'/authorizations',
],
},
],
},
{
sectionKey: 'queues',
items: [
{
id: 'awooop-work-items',
href: '/awooop/work-items',
labelKey: 'workItems',
Icon: ClipboardList,
},
{
id: 'awooop-runs',
href: '/awooop/runs',
labelKey: 'runMonitor',
Icon: PlayCircle,
},
{
id: 'awooop-approvals',
href: '/awooop/approvals',
labelKey: 'approvalQueue',
Icon: ShieldCheck,
aliases: ['/authorizations'],
badge: true,
},
{
id: 'awooop-contracts',
href: '/awooop/contracts',
labelKey: 'contracts',
Icon: FileText,
},
{
id: 'awooop-tenants',
href: '/awooop/tenants',
labelKey: 'tenants',
Icon: Building2,
},
],
},
{
sectionKey: 'monitoring',
items: [
{
id: 'observability',
href: '/observability',
@@ -107,11 +146,11 @@ const NAV_SECTIONS: NavSection[] = [
aliases: ['/monitoring', '/apm', '/errors', '/apps', '/services', '/topology'],
},
{
id: 'knowledge-automation',
href: '/knowledge-base',
labelKey: 'knowledgeAutomation',
Icon: BookOpen,
aliases: ['/knowledge', '/automation', '/auto-repair', '/drift', '/neural-command'],
id: 'alerts',
href: '/alerts',
labelKey: 'alerts',
Icon: Bell,
aliases: ['/alert-operation-logs', '/notifications'],
},
{
id: 'iwooos-security',
@@ -119,14 +158,38 @@ const NAV_SECTIONS: NavSection[] = [
labelKey: 'iwooos',
Icon: ShieldCheck,
aliases: ['/security-compliance'],
relatedPaths: ['/iwooos', '/code-review', '/security', '/compliance'],
relatedPaths: ['/iwooos', '/security', '/compliance'],
},
{
id: 'code-review',
href: '/code-review',
labelKey: 'codeReview',
Icon: GitPullRequest,
},
],
},
{
sectionKey: 'knowledge',
items: [
{
id: 'knowledge-automation',
href: '/knowledge-base',
labelKey: 'knowledgeAutomation',
Icon: BookOpen,
aliases: ['/knowledge', '/automation', '/auto-repair', '/drift', '/neural-command'],
},
{
id: 'reports',
href: '/reports',
labelKey: 'reports',
Icon: BarChart3,
},
{
id: 'operations',
href: '/operations',
labelKey: 'operationsOverview',
Icon: Package,
aliases: ['/deployments', '/tickets', '/cost', '/billing', '/action-logs', '/reports'],
aliases: ['/deployments', '/tickets', '/cost', '/billing', '/action-logs'],
},
],
},

View File

@@ -95,6 +95,55 @@ export class ApiError extends Error {
}
}
export type IwoooSRuntimeSecurityReadbackTone = 'steady' | 'warn' | 'locked'
export interface IwoooSRuntimeSecurityReadbackLane {
lane_id:
| 'wazuh_registry'
| 'wazuh_dashboard_api'
| 'kali_intake'
| 'alert_readability'
| 'owner_dispatch'
| 'intrusion_prevention'
status: string
completion_percent: number
tone: IwoooSRuntimeSecurityReadbackTone
next_gate: string
metrics: Record<string, number>
source_refs: string[]
}
export interface IwoooSRuntimeSecurityReadbackResponse {
schema_version: 'iwooos_runtime_security_readback_v1'
status: string
mode: string
source_refs: string[]
summary: {
source_snapshot_count: number
p0_lane_count: number
control_plane_visibility_percent: number
actual_runtime_acceptance_percent: number
owner_response_received_count: number
owner_response_accepted_count: number
redacted_evidence_refs_received_count: number
request_sent_count: number
wazuh_expected_host_scope_count: number
wazuh_manager_registry_accepted_count: number
wazuh_transport_observed_count: number
wazuh_dashboard_api_degraded_observed_count: number
kali_active_scan_authorized_count: number
kali_execute_authorized_count: number
kali_finding_envelope_accepted_count: number
alert_formatter_contract_marker_count: number
alert_receipt_runtime_send_count: number
intrusion_prevention_candidate_count: number
runtime_gate_count: number
}
lanes: IwoooSRuntimeSecurityReadbackLane[]
boundaries: Record<string, boolean>
no_false_green_rules: string[]
}
async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const error = await response.json().catch(() => ({}))
@@ -128,6 +177,11 @@ export const apiClient = {
}>(res)
},
async getIwoooSRuntimeSecurityReadback() {
const res = await fetch(`${API_BASE_URL}/iwooos/runtime-security-readback`, { cache: 'no-store' })
return handleResponse<IwoooSRuntimeSecurityReadbackResponse>(res)
},
// Agent
async getAgentStatus() {
const res = await fetch(`${API_BASE_URL}/agent/status`)

View File

@@ -1,3 +1,34 @@
## 2026-06-26IwoooS Runtime 資安讀回總板Wazuh / Kali / 告警 / owner dispatch 六條 P0 線接上只讀 API 與前台
**背景**IwoooS 資安工作不能停在文件、靜態卡片或截圖Wazuh、資安觀測節點、告警可讀性、owner dispatch、外部入侵防護與 runtime gate 必須能被前台讀回,同時不得把 UI 可見、route 200、transport count 或一般操作批准誤判成 runtime 授權。
**完成**
- 新增 `iwooos_runtime_security_readback_v1` 後端只讀 service彙整 8 份已提交資安 snapshotS4.9 owner gap、Wazuh managed host coverage、Wazuh agent visibility runtime gate、Kali integration、SOC/SIEM/Kali/Wazuh integration、Telegram alert readability、monitoring owner request draft、external host intrusion prevention control。
- 新增 `GET /api/v1/iwooos/runtime-security-readback`;端點只讀 repo snapshot不呼叫 Wazuh live API、不呼叫 Kali、不 SSH、不碰 Docker / Nginx / firewall / Telegram / secret。
- 前台 `/zh-TW/iwooos` 新增 `IwoooS Runtime 資安讀回` 總板,顯示六條 P0 線:`wazuh_registry``wazuh_dashboard_api``kali_intake``alert_readability``owner_dispatch``intrusion_prevention`
- `zh-TW` 與目前鏡像 `en` 都新增繁中產品文案;新增 API client 型別與 `getIwoooSRuntimeSecurityReadback()`
**只讀驗證結果**
- `pytest apps/api/tests/test_iwooos_runtime_security_readback.py apps/api/tests/test_iwooos_wazuh_api.py -q``9 passed`
- `python3.11 -m py_compile apps/api/src/api/v1/iwooos.py apps/api/src/services/iwooos_runtime_security_readback.py apps/api/src/services/snapshot_paths.py`:通過。
- `pnpm --dir apps/web typecheck`:通過;本輪先以離線 pnpm store 補最小依賴連結,未下載新套件。
- `python3 scripts/security/security-mirror-progress-guard.py --root .``SECURITY_MIRROR_PROGRESS_GUARD_OK`
- `python3 scripts/security/source-control-owner-response-guard.py --root .``SOURCE_CONTROL_OWNER_RESPONSE_GUARD_OK`
- `python3 scripts/ops/doc-secrets-sanity-check.py docs/security docs/templates apps/web/messages docs/LOGBOOK.md 'apps/web/src/app/[locale]/iwooos/page.tsx' apps/web/src/lib/api-client.ts apps/api/src/api/v1/iwooos.py apps/api/src/services/iwooos_runtime_security_readback.py``DOC_SECRET_SANITY_OK scanned_files=274`
- `git diff --check`:通過。
- readback summary`control_plane_visibility_percent=84``actual_runtime_acceptance_percent=0``runtime_gate_count=0``owner_response_received_count=0``owner_response_accepted_count=0``wazuh_manager_registry_accepted_count=0``kali_active_scan_authorized_count=0``alert_receipt_runtime_send_count=0`
**完成度同步**
- 後端 readback API source`100%`
- 前台 IwoooS runtime readback source`100%`
- 本地驗證:`95%`;尚未做 production deploy / desktop / mobile 驗證。
- IwoooS runtime acceptance`0%`
- Wazuh manager registry accepted`0`
- Owner response received / accepted`0 / 0`
- Active runtime gate / active scan / active response / Telegram send`0 / false`
**邊界**:本段只做只讀 API、前台讀回與測試不主動修 Wazuh、不重新註冊 agent、不重啟 manager / dashboard、不啟動 Kali scan、不執行 `/execute`、不改 Nginx、不 reload、不改 firewall、不收 secret、不送 Telegram、不開 runtime gate。
## 2026-06-26AwoooP Owner release AI 預填草案正式上線:把人工處理縮成決策確認,不再整包丟回值班者
**背景**:使用者指出 `node-exporter-110` 類 Telegram 告警仍顯示 `manual_required` / `DRAFT_READY`,但最後看起來仍要人工處理所有欄位。前一段已把 Work Items 顯示到 apply gate readiness本段進一步把 `execution_release_contract` 中可由 AI 先產出的 owner release 資訊預填,讓 operator 看到 AI 已準備哪些欄位、人工只剩哪些決策不再把維護窗口、rollback、blast radius、verifier、KM / PlayBook trust 全部標成 raw missing。
@@ -45596,3 +45627,40 @@ production browser smoke:
- DR credential escrow evidence 仍缺 `5`:不得宣稱 `DR_COMPLETE`
- Wazuh manager registry accepted 仍為 `0`:不得宣稱 Wazuh 全主機納管恢復。
- certbot formal renewal 尚未完成 readback本輪完成的是 HTTP-01 route / timer hygiene / failed-unit 清除,正式 renew 成功需等 snap certbot timer 或獨立 ACME window。
## 2026-06-26 — 13:01 post-reboot owner response preflight / SOP v1.74
**時間與來源**
- 2026-06-26 13:01-13:23 Asia/Taipei。
- 來源:`scripts/reboot-recovery/post-reboot-next-gate-owner-packets.py --no-color`、新增 `scripts/reboot-recovery/post-reboot-owner-response-preflight.py --no-color`、placeholder template `docs/templates/post-reboot-next-gate-owner-response.json`、SOP / workplan 文件同步。
**完成內容**
- 新增 post-reboot owner response preflight驗收未來 owner response JSON 是否符合目前 `awoooi_post_reboot_next_gate_owner_packets_v1` 的動態 gate set。
- 新增 placeholder response template刻意保留 `owner_role_here``non_secret_evidence_ref_here``registry_export_ref_here` 等 placeholder作為 fail-closed 測試樣本;直接套用模板不得被算成已收件或已接受。
- `docs/runbooks/REBOOT-POST-START-QUICK-CHECK.md` 升至 v1.14,固定流程改為 summary → declaration guard → next-gate dispatch → owner packet → contract guard → owner response preflight。
- `docs/runbooks/FULL-STACK-COLD-START-SOP.md` 升至 v1.74,將 owner response preflight 納入完整開機 / 關機 / 重啟 SOP。
- `docs/workplans/2026-06-04-reboot-cold-start-backup-recovery-workplan.md` 更新為 `DONE_WITH_OWNER_RESPONSE_PREFLIGHT_V174`
**live / preflight 證據**
- 13:23 owner packet live generation 讀回 `next_gate_count=2`,只剩 `credential_escrow_evidence``wazuh_manager_registry_export``request_sent_count=0``owner_response_received_count=0``owner_response_accepted_count=0``runtime_action_authorized_count=0`
- 12:58 post-start summary 已恢復為 `POST_START_RESULT=FULL_STACK_GREEN_DR_ESCROW_BLOCKED``POST_START_PASS=38``POST_START_WARN=4``POST_START_BLOCKED=0``SERVICE_GREEN=1``PRODUCT_DATA_GREEN=1``BACKUP_CORE_GREEN=1``DR_ESCROW_BLOCKED=1``ESCROW_MISSING_COUNT=5``HOST_188_HYGIENE_BLOCKED=0``HOST_188_RESULT=HOST_188_HYGIENE_GREEN.``WAZUH_ROUTE_CODE=200``WAZUH_TRANSPORT_COUNT=6``WAZUH_COVERAGE_SCOPE=6``WAZUH_DIRECT_ACTIVE=2``WAZUH_NO_TRANSPORT=1``WAZUH_SSH_BLOCKED=3``WAZUH_DASHBOARD_API_CONNECTION=pending_or_spinning``WAZUH_DASHBOARD_INDEX_OK=3``WAZUH_MANAGER_REGISTRY_ACCEPTED=0``RUNTIME_ACTION_AUTHORIZED=0``OVERALL_DECLARATION=FULL_STACK_GREEN_DR_ESCROW_BLOCKED``NEXT_REQUIRED_GATES=credential_escrow_evidence,wazuh_manager_registry_export`
- 12:55 首輪 owner-packet generation 曾因 110 transient `stockplatform-review-bulk-ux` active process / service warning 使 summary 暫時落入 service warning未 kill、未 restart、未取消 CI12:58 重跑後自動恢復,證明 SOP 會把 transient / active CI process 與真正 orphan / service blocker 分開。
- 無 response file 預期輸出:`POST_REBOOT_OWNER_RESPONSE_PREFLIGHT_BLOCKED status=blocked_waiting_owner_response_file expected_gates=2 received=0 accepted=0 runtime_gate=0 blockers=1`
- placeholder template 輸出:`POST_REBOOT_OWNER_RESPONSE_PREFLIGHT_BLOCKED status=blocked_waiting_owner_response_content expected_gates=2 received=0 accepted=0 runtime_gate=0 blockers=41`
**做過的命令類型**
- 只讀post-reboot owner packet generation、owner response preflight、contract / declaration / source guards。
- 寫入repo script / docs / template only。
- 未做:沒有 host / Docker / systemd / Nginx / firewall / K8s / DB / Wazuh runtime 寫操作;沒有讀 secret 明文;沒有寫 credential marker沒有送 owner request沒有 Wazuh active response / agent re-enroll / restart沒有 Kali active scan。
**目前判定**
- Owner response preflight automation`0% -> 100%`
- Reboot service / product data / backup / 188 host hygiene`GREEN`
- Overall recovery declaration`FULL_STACK_GREEN_DR_ESCROW_BLOCKED`
- SOP / quick-check / owner-packet / owner-response preflightv1.74。
**仍 blocked / 不得宣稱**
- DR credential escrow evidence 仍缺 `5`:不得宣稱 `DR_COMPLETE` 或 credential escrow complete。
- Wazuh manager registry accepted 仍為 `0`:不得宣稱 Wazuh 全主機納管恢復。
- Owner response received / accepted 仍為 `0 / 0`不得把「批准繼續」、空模板、UI 可見、route `200`、transport `6`、Dashboard index pattern `3` 或 owner-packet JSON 當成 evidence accepted。
- Runtime action / host write / credential marker write / Wazuh active response / Kali active scan 仍全部 `0 / false`

View File

@@ -1,6 +1,6 @@
# AWOOOI 全棧冷啟動與主機重啟 SOP
> Version: v1.73
> Version: v1.74
> Last updated: 2026-06-26 Asia/Taipei
> Scope: 110 / 120 / 121 / 188 full-stack reboot recovery. 112 Kali is recorded as P3 optional and is not part of this recovery path.
@@ -10,10 +10,12 @@
本節是每次接手、開機、關機、重啟後的第一個判定錨點。若日期不是今天,必須先重跑 live check再更新本節與 `docs/workplans/2026-06-04-reboot-cold-start-backup-recovery-workplan.md`
若只是重啟後要快速判斷能不能宣稱恢復,先跑機器可讀摘要:`scripts/reboot-recovery/post-reboot-readiness-summary.sh --no-color`。此腳本會呼叫一頁式總檢查、188 host hygiene checklist 與 Wazuh no-false-green repo gates並把 delegated logs 留在 `/tmp/awoooi-post-reboot-readiness-*`。接著跑 `scripts/reboot-recovery/post-reboot-declaration-guard.py --no-color`,把 summary 轉成 allowed / forbidden declaration避免把服務綠誤報成 DR complete、188 host hygiene、Wazuh registry recovered 或 runtime authorized。若 summary 顯示 `SERVICE_GREEN=1``NEXT_REQUIRED_GATES` 仍非空,再跑 `scripts/reboot-recovery/post-reboot-next-gate-dispatch.sh --no-color`,把 live summary 內尚未完成的 blocker 轉成 owner / evidence / forbidden-action dispatch checklist需要機器可讀 intake 時,再跑 `scripts/reboot-recovery/post-reboot-next-gate-owner-packets.py --no-color --output /tmp/awoooi-post-reboot-owner-packets.json` 產生 `awoooi_post_reboot_next_gate_owner_packets_v1` JSON並立刻跑 `scripts/reboot-recovery/post-reboot-owner-packet-contract-guard.py --packet-file /tmp/awoooi-post-reboot-owner-packets.json`。dispatch / packet / guard 均固定 `DISPATCH_AUTHORIZED=0``REQUEST_SENT_COUNT=0``OWNER_RESPONSE_ACCEPTED=0``HOST_WRITE_AUTHORIZED=0``SECRET_VALUE_COLLECTION_ALLOWED=0``RUNTIME_GATE=0`guard 未通過時不得送 owner request、不得寫 escrow marker、不得進維護窗口、不得宣稱 DR / Wazuh registry complete。需要人工展開時再跑 `scripts/reboot-recovery/post-start-quick-check.sh --no-color` 並以 `docs/runbooks/REBOOT-POST-START-QUICK-CHECK.md` 作為 fallback。長 SOP 保留完整背景、例外處理與 Plan B短版 wrapper / checklist 負責每次 T+10 分鐘內的固定判定。
若只是重啟後要快速判斷能不能宣稱恢復,先跑機器可讀摘要:`scripts/reboot-recovery/post-reboot-readiness-summary.sh --no-color`。此腳本會呼叫一頁式總檢查、188 host hygiene checklist 與 Wazuh no-false-green repo gates並把 delegated logs 留在 `/tmp/awoooi-post-reboot-readiness-*`。接著跑 `scripts/reboot-recovery/post-reboot-declaration-guard.py --no-color`,把 summary 轉成 allowed / forbidden declaration避免把服務綠誤報成 DR complete、188 host hygiene、Wazuh registry recovered 或 runtime authorized。若 summary 顯示 `SERVICE_GREEN=1``NEXT_REQUIRED_GATES` 仍非空,再跑 `scripts/reboot-recovery/post-reboot-next-gate-dispatch.sh --no-color`,把 live summary 內尚未完成的 blocker 轉成 owner / evidence / forbidden-action dispatch checklist需要機器可讀 intake 時,再跑 `scripts/reboot-recovery/post-reboot-next-gate-owner-packets.py --no-color --output /tmp/awoooi-post-reboot-owner-packets.json` 產生 `awoooi_post_reboot_next_gate_owner_packets_v1` JSON並立刻跑 `scripts/reboot-recovery/post-reboot-owner-packet-contract-guard.py --packet-file /tmp/awoooi-post-reboot-owner-packets.json`。dispatch / packet / guard 均固定 `DISPATCH_AUTHORIZED=0``REQUEST_SENT_COUNT=0``OWNER_RESPONSE_ACCEPTED=0``HOST_WRITE_AUTHORIZED=0``SECRET_VALUE_COLLECTION_ALLOWED=0``RUNTIME_GATE=0`guard 未通過時不得送 owner request、不得寫 escrow marker、不得進維護窗口、不得宣稱 DR / Wazuh registry complete。v1.74 起,任何 owner response JSON 還必須經過 `scripts/reboot-recovery/post-reboot-owner-response-preflight.py --no-color --response-file <file>`空模板、placeholder、secret payload、runtime action request、credential marker write、Wazuh active response / re-enroll / restart、Kali active scan 或缺少 Dashboard API / manager registry evidence 都必須 fail-closedpreflight 通過也只表示可進入獨立 reviewer acceptance不是 runtime 授權。需要人工展開時,再跑 `scripts/reboot-recovery/post-start-quick-check.sh --no-color` 並以 `docs/runbooks/REBOOT-POST-START-QUICK-CHECK.md` 作為 fallback。長 SOP 保留完整背景、例外處理與 Plan B短版 wrapper / checklist 負責每次 T+10 分鐘內的固定判定。
2026-06-26 12:13 latest live summary supersedes the 08:59 gate set`scripts/reboot-recovery/post-reboot-readiness-summary.sh --no-color` 回傳 `POST_START_RESULT=FULL_STACK_GREEN_DR_ESCROW_BLOCKED``POST_START_PASS=38``POST_START_WARN=4``POST_START_BLOCKED=0``SERVICE_GREEN=1``PRODUCT_DATA_GREEN=1``BACKUP_CORE_GREEN=1``DR_ESCROW_BLOCKED=1``ESCROW_MISSING_COUNT=5``HOST_188_SERVICE_GREEN=1``HOST_188_HYGIENE_BLOCKED=0``HOST_188_RESULT=HOST_188_HYGIENE_GREEN.``WAZUH_ROUTE_CODE=200``WAZUH_TRANSPORT_COUNT=6``WAZUH_MANAGER_REGISTRY_ACCEPTED=0``WAZUH_DASHBOARD_API_CONNECTION=pending_or_spinning``WAZUH_DASHBOARD_INDEX_OK=3``RUNTIME_ACTION_AUTHORIZED=0``OVERALL_DECLARATION=FULL_STACK_GREEN_DR_ESCROW_BLOCKED``NEXT_REQUIRED_GATES=credential_escrow_evidence,wazuh_manager_registry_export`。188 host hygiene 已從 blocker 移除;目前不可宣稱完成的只剩 DR credential escrow 與 Wazuh manager registry。ACME HTTP-01 route 與 certbot timer hygiene 已修復,但不得宣稱憑證已正式 renew需等 snap certbot timer / ACME window readback。
2026-06-26 13:01 owner response preflight baseline新增 `scripts/reboot-recovery/post-reboot-owner-response-preflight.py --no-color``docs/templates/post-reboot-next-gate-owner-response.json`。無 response file 時必須輸出 `POST_REBOOT_OWNER_RESPONSE_PREFLIGHT_BLOCKED status=blocked_waiting_owner_response_file expected_gates=2 received=0 accepted=0 runtime_gate=0`;直接使用模板時必須輸出 `POST_REBOOT_OWNER_RESPONSE_PREFLIGHT_BLOCKED status=blocked_waiting_owner_response_content expected_gates=2 received=0 accepted=0 runtime_gate=0`。此 gate 只驗收 `credential_escrow_evidence``wazuh_manager_registry_export` 的脫敏 owner evidence不送 request、不寫 escrow marker、不讀 secret、不做 Wazuh / host / Kali runtime action也不把一般批准訊息轉成 owner accepted。
2026-06-26 07:47 machine-readable readiness summary retained as historical pre-repair evidence當時 `HOST_188_HYGIENE_BLOCKED=1``NEXT_REQUIRED_GATES=credential_escrow_evidence,host_188_hygiene_maintenance_window,wazuh_manager_registry_export`。此段只用來比對 188 修復前後差異;現行 gate set 必須使用 12:13 baseline。
2026-06-26 08:12 next-gate dispatch baseline retained as historical pre-repair evidence當時 output 固定三個 P0 checklist。12:13 起 dispatch 依 live summary 動態輸出,目前 expected `NEXT_GATE_COUNT=2`,只剩 credential escrow 與 Wazuh registry。

View File

@@ -1,6 +1,6 @@
# 主機重啟後一頁式總檢查
> Version: v1.13
> Version: v1.14
> Last updated: 2026-06-26 Asia/Taipei
> Scope: 110 / 120 / 121 / 188 post-reboot service recovery. 112 Kali / Wazuh / active scan 不屬於本流程。
@@ -10,7 +10,7 @@
每次 110 / 120 / 121 / 188 任一台主機開機、關機、重啟、斷電恢復、VMware console fsck、Docker / K3s 大量重排後,都先跑本頁,再決定是否宣稱恢復。
最新基準2026-06-26 12:13 post-reboot summary / declaration guard`scripts/reboot-recovery/post-reboot-readiness-summary.sh --no-color` 回傳 `SERVICE_GREEN=1``PRODUCT_DATA_GREEN=1``BACKUP_CORE_GREEN=1``DR_ESCROW_BLOCKED=1``ESCROW_MISSING_COUNT=5``HOST_188_HYGIENE_BLOCKED=0``HOST_188_RESULT=HOST_188_HYGIENE_GREEN.``WAZUH_MANAGER_REGISTRY_ACCEPTED=0``WAZUH_COVERAGE_SCOPE=6``WAZUH_DIRECT_ACTIVE=2``WAZUH_NO_TRANSPORT=1``WAZUH_SSH_BLOCKED=3``WAZUH_ROUTE_CODE=200``WAZUH_TRANSPORT_COUNT=6``WAZUH_DASHBOARD_API_CONNECTION=pending_or_spinning``WAZUH_DASHBOARD_INDEX_OK=3``RUNTIME_ACTION_AUTHORIZED=0``OVERALL_DECLARATION=FULL_STACK_GREEN_DR_ESCROW_BLOCKED``scripts/reboot-recovery/post-reboot-declaration-guard.py --no-color` 會把 summary 轉成 allowed / forbidden declaration目前允許宣稱服務、產品資料、備份核心、188 host hygiene green 與 `FULL_STACK_GREEN_DR_ESCROW_BLOCKED`;禁止宣稱 `DR_COMPLETE``WAZUH_REGISTRY_RECOVERED``RUNTIME_ACTION_AUTHORIZED`。接著 `scripts/reboot-recovery/post-reboot-next-gate-dispatch.sh --no-color``NEXT_REQUIRED_GATES=credential_escrow_evidence,wazuh_manager_registry_export` 展成 owner / evidence / forbidden-action checklistWazuh checklist 的 `CURRENT_EVIDENCE` 會保留 registry accepted、coverage scope、direct active、no transport、SSH blocked、route、transport、Dashboard API 與 index pattern 狀態,避免把 route `200` 或 transport `6` 誤報成 registry recovered。`scripts/reboot-recovery/post-reboot-next-gate-owner-packets.py --no-color` 進一步轉成 `awoooi_post_reboot_next_gate_owner_packets_v1` JSON固定 `dispatch_authorized=0``request_sent_count=0``owner_response_accepted_count=0``host_write_authorized=0``secret_value_collection_allowed=0``runtime_gate_count=0``scripts/reboot-recovery/post-reboot-owner-packet-contract-guard.py --packet-file /tmp/awoooi-post-reboot-owner-packets.json` 依 live `next_required_gates` 動態鎖定 P0 gate、所有 `0 / false` 邊界、禁用 secret payload / runtime action 與 no-false-green 規則。DR 仍因 `escrow_missing=5` 不可宣稱 completeWazuh manager registry 仍是 service green 之外的獨立 blocker。ACME HTTP-01 route / certbot timer hygiene 已修復,但憑證正式 renew 成功需等 snap certbot timer 或獨立 ACME window readback。
最新基準2026-06-26 13:01 post-reboot owner response preflight`scripts/reboot-recovery/post-reboot-readiness-summary.sh --no-color` 回傳 `SERVICE_GREEN=1``PRODUCT_DATA_GREEN=1``BACKUP_CORE_GREEN=1``DR_ESCROW_BLOCKED=1``ESCROW_MISSING_COUNT=5``HOST_188_HYGIENE_BLOCKED=0``HOST_188_RESULT=HOST_188_HYGIENE_GREEN.``WAZUH_MANAGER_REGISTRY_ACCEPTED=0``WAZUH_COVERAGE_SCOPE=6``WAZUH_DIRECT_ACTIVE=2``WAZUH_NO_TRANSPORT=1``WAZUH_SSH_BLOCKED=3``WAZUH_ROUTE_CODE=200``WAZUH_TRANSPORT_COUNT=6``WAZUH_DASHBOARD_API_CONNECTION=pending_or_spinning``WAZUH_DASHBOARD_INDEX_OK=3``RUNTIME_ACTION_AUTHORIZED=0``OVERALL_DECLARATION=FULL_STACK_GREEN_DR_ESCROW_BLOCKED``scripts/reboot-recovery/post-reboot-declaration-guard.py --no-color` 會把 summary 轉成 allowed / forbidden declaration目前允許宣稱服務、產品資料、備份核心、188 host hygiene green 與 `FULL_STACK_GREEN_DR_ESCROW_BLOCKED`;禁止宣稱 `DR_COMPLETE``WAZUH_REGISTRY_RECOVERED``RUNTIME_ACTION_AUTHORIZED`。接著 `scripts/reboot-recovery/post-reboot-next-gate-dispatch.sh --no-color``NEXT_REQUIRED_GATES=credential_escrow_evidence,wazuh_manager_registry_export` 展成 owner / evidence / forbidden-action checklistWazuh checklist 的 `CURRENT_EVIDENCE` 會保留 registry accepted、coverage scope、direct active、no transport、SSH blocked、route、transport、Dashboard API 與 index pattern 狀態,避免把 route `200` 或 transport `6` 誤報成 registry recovered。`scripts/reboot-recovery/post-reboot-next-gate-owner-packets.py --no-color` 進一步轉成 `awoooi_post_reboot_next_gate_owner_packets_v1` JSON固定 `dispatch_authorized=0``request_sent_count=0``owner_response_accepted_count=0``host_write_authorized=0``secret_value_collection_allowed=0``runtime_gate_count=0``scripts/reboot-recovery/post-reboot-owner-packet-contract-guard.py --packet-file /tmp/awoooi-post-reboot-owner-packets.json` 依 live `next_required_gates` 動態鎖定 P0 gate、所有 `0 / false` 邊界、禁用 secret payload / runtime action 與 no-false-green 規則。新增 `scripts/reboot-recovery/post-reboot-owner-response-preflight.py --no-color` 作為 owner response 收件預檢:沒有 response file 必須是 `blocked_waiting_owner_response_file`;直接套用 `docs/templates/post-reboot-next-gate-owner-response.json` 必須是 `blocked_waiting_owner_response_content`;只有具備遮罩 evidence refs、完整 owner 欄位、Wazuh registry / Dashboard API 狀態、五個 credential escrow 非 secret evidence refs且沒有 secret value / runtime action request 的 response 才能進入下一層 reviewer acceptance。DR 仍因 `escrow_missing=5` 不可宣稱 completeWazuh manager registry 仍是 service green 之外的獨立 blocker。ACME HTTP-01 route / certbot timer hygiene 已修復,但憑證正式 renew 成功需等 snap certbot timer 或獨立 ACME window readback。
本頁只回答四件事:
@@ -100,6 +100,15 @@ scripts/reboot-recovery/post-reboot-owner-packet-contract-guard.py --packet-file
guard 必須輸出 `POST_REBOOT_OWNER_PACKET_CONTRACT_GUARD_OK gates=<live_next_gate_count> request_sent=0 accepted=0 runtime_gate=0`。目前預期 `gates=2`;若 188 hygiene 回到 blocked才會是 `gates=3`。若 gate 數量、P0 gate id、`0 / false` 欄位、禁用 secret payload、Wazuh 禁用 active response / host write或 no-false-green 規則任何一項漂移,視為 `BLOCKED`,不得送 owner request、不得寫 escrow marker、不得進維護窗口、不得宣稱 DR / Wazuh 完成。
收到 owner response 檔案前,或收到任何聲稱已補證據的 JSON 前,必須跑 owner response preflight
```bash
scripts/reboot-recovery/post-reboot-owner-response-preflight.py --no-color
scripts/reboot-recovery/post-reboot-owner-response-preflight.py --no-color --response-file docs/templates/post-reboot-next-gate-owner-response.json
```
第一個命令必須輸出 `POST_REBOOT_OWNER_RESPONSE_PREFLIGHT_BLOCKED status=blocked_waiting_owner_response_file expected_gates=2 received=0 accepted=0 runtime_gate=0`。第二個命令必須輸出 `POST_REBOOT_OWNER_RESPONSE_PREFLIGHT_BLOCKED status=blocked_waiting_owner_response_content expected_gates=2 received=0 accepted=0 runtime_gate=0`,證明空模板不能被算成已收件或已接受。合格 response 只能包含脫敏 evidence refs、owner role / team / decision / reviewer / followup owner、五個 escrow item 的 non-secret evidence ref以及 Wazuh manager registry / Dashboard API readback不得包含密碼、token、secret value、hash、prefix/suffix、raw Wazuh payload、agent 原名、內網 IP、`client.keys`、active response、host write、agent re-enroll、Wazuh restart、Kali active scan 或 credential marker write。preflight 通過也只代表可進入獨立 reviewer acceptance不代表 `DR_COMPLETE``WAZUH_REGISTRY_RECOVERED` 或任何 runtime action 授權。
需要展開細節時,再使用 repo-side wrapper
```bash

View File

@@ -0,0 +1,98 @@
{
"schema_version": "awoooi_post_reboot_next_gate_owner_response_v1",
"responses": [
{
"gate_id": "credential_escrow_evidence",
"owner_role": "owner_role_here",
"owner_team": "owner_team_here",
"decision": "pending",
"decision_reason": "decision_reason_here",
"affected_scope": "AWOOOI DR credential escrow non-secret evidence",
"redacted_evidence_refs": [
"redacted_evidence_ref_here"
],
"followup_owner": "followup_owner_here",
"runtime_action_requested": false,
"host_write_requested": false,
"secret_value_included": false,
"secret_value_collection_allowed": false,
"credential_marker_write_requested": false,
"escrow_items": [
{
"item_id": "restic_repository_password",
"non_secret_evidence_ref": "non_secret_evidence_ref_here",
"recovery_owner": "owner_role_here",
"reviewer": "reviewer_here",
"last_reviewed_at": "pending",
"contains_secret_value": false
},
{
"item_id": "offsite_provider_credentials",
"non_secret_evidence_ref": "non_secret_evidence_ref_here",
"recovery_owner": "owner_role_here",
"reviewer": "reviewer_here",
"last_reviewed_at": "pending",
"contains_secret_value": false
},
{
"item_id": "break_glass_admin_credentials",
"non_secret_evidence_ref": "non_secret_evidence_ref_here",
"recovery_owner": "owner_role_here",
"reviewer": "reviewer_here",
"last_reviewed_at": "pending",
"contains_secret_value": false
},
{
"item_id": "dns_registrar_recovery",
"non_secret_evidence_ref": "non_secret_evidence_ref_here",
"recovery_owner": "owner_role_here",
"reviewer": "reviewer_here",
"last_reviewed_at": "pending",
"contains_secret_value": false
},
{
"item_id": "oauth_ai_provider_recovery",
"non_secret_evidence_ref": "non_secret_evidence_ref_here",
"recovery_owner": "owner_role_here",
"reviewer": "reviewer_here",
"last_reviewed_at": "pending",
"contains_secret_value": false
}
]
},
{
"gate_id": "wazuh_manager_registry_export",
"owner_role": "owner_role_here",
"owner_team": "owner_team_here",
"decision": "pending",
"decision_reason": "decision_reason_here",
"affected_scope": "Wazuh manager registry redacted export",
"redacted_evidence_refs": [
"redacted_evidence_ref_here"
],
"followup_owner": "followup_owner_here",
"runtime_action_requested": false,
"host_write_requested": false,
"secret_value_included": false,
"secret_value_collection_allowed": false,
"wazuh_active_response_requested": false,
"agent_reenroll_requested": false,
"wazuh_restart_requested": false,
"kali_active_scan_requested": false,
"registry_export_ref": "registry_export_ref_here",
"registry_time_window": "pending",
"expected_host_aliases": [
"core-110",
"gateway-188",
"k3s-control-120",
"k3s-control-121",
"security-observer-112",
"dev-workstation-111"
],
"manager_registry_count": 0,
"dashboard_api_connection_status": "pending",
"dashboard_api_version_status": "pending",
"reviewer": "reviewer_here"
}
]
}

View File

@@ -15,7 +15,7 @@
| P0 host / K3s recovery | DONE | 100% | 120 booted after console fsck at `2026-06-12 15:13`; latest 2026-06-26 07:19 readback shows 120 and 121 reachable, K3s active, `mon` and `mon1` both `Ready control-plane`, AWOOOI API/Web replicas split across both nodes, ArgoCD `awoooi-prod Synced / Healthy` at revision `1fd5e2a8b0f18d24eed16aa2a44286bcbf230603`, and `km-vectorize` official 03:00 台北時間 run succeeded with `lastSuccess=2026-06-25T19:00:14Z`. |
| P1 backup / alert / escrow | BLOCKED_DR_ESCROW | 97% | 2026-06-26 06:58 backup readback shows 110 `13/13 fresh failed=0`, 188 `2/2 fresh failed=0`, `core_blockers=0`, `integrity_stale=0`, `offsite_fresh=1`, `rclone_gdrive_fresh=1`, `escrow_missing=5`, last aggregate `2026-06-26 02:31:02`。DR remains blocked on real non-secret credential escrow evidence IDs; do not write placeholder markers or paste secret values. |
| P2 service / data truth | DONE | 100% | Service routes and core runtime are available, 110 current CPU pressure is attributable to active AWOOOI Web `turbo build` / Docker buildx, and previous orphan Chrome groups remain cleared. 2026-06-26 07:19 StockPlatform `/api/v1/system/freshness` returned `200`; 07:01 freshness payload was `status=ok`, `latest_trading_date=2026-06-25`, blockers `[]`; price / chips / margin / AI recommendations are all on `2026-06-25`. `ai.recommendations` row count is `2868`; `core.margin_short_daily` row count is `1976`. MOMO health `V10.699`, current-month parity `15383|15383|2026-06-01|2026-06-24|2026-06-01|2026-06-24`, and `MOMO_DAILY_FRESHNESS 1|2026-06-24` are green; expanded public routes are green. |
| P3 docs / automation contracts | DONE_WITH_DECLARATION_GUARD_V173 | 100% | Workplan, SOP v1.73, post-reboot declaration guard, machine-readable post-reboot readiness summary with Wazuh registry detail fields, post-reboot next-gate dispatch checklist, owner-packet JSON generator, dynamic owner-packet contract guard, one-page post-start quick check v1.13, route retry gate, deploy warmup classification, expanded public route list, StockPlatform freshness gate, StockPlatform cron-source recovery evidence, StockPlatform natural schedule green evidence, 110 orphan Chrome recurrence cleanup evidence, 188 fail-closed startup data recovery gate, 188 host hygiene read-only checklist, 188 PostgreSQL runtime-ready source-of-truth, 188 ACME route/timer hygiene, baseline `stockplatform_system_freshness_ok`, BACKUP-STATUS, LOGBOOK, 120 console/fsck recovery, Gitea backup stale-dump hardening, reboot ledger/version-comparison SOP, escrow evidence audit, 188 nginx Ansible baseline, 110 cold-start detector script, startup judgment layers, GO/NO-GO tree, host recovery cards, explicit Plan B degraded-operation path, machine-readable `plan_b` baseline, readiness-audit Plan B guard, B0-B5 service levels, T+0/T+120 fallback timeline checks, host role / load-balancing assessment, CD `known_hosts` guardrail, `fwupd-refresh.timer` rollback note, K3s filesystem event blocker, AWOOOI backup no-direct-offsite-sync contract, 110/188 Ansible source-of-truth, Gitea self-hosted readiness validation workflow, post-CD no-regression readbacks, stale-vs-active K8s failed Job classification, 110 runaway browser / CI load AIOps exporter + alert + gated remediation PlayBook, Telegram / AI event packet mapping, healthy heartbeat Telegram suppression, MOMO scheduler / current-month detector fix, exporter restore helpers, 110 Docker disk pressure cleanup boundary, notification-noise readback, MOMO import-boundary / Drive-auth fail-closed deploys, product version/readback matrix, and stricter product-data / route retry gates are updated. Declaration guard now machine-checks allowed / forbidden recovery statements: service/data/backup/188 host hygiene green may be declared when live summary says so, while `DR_COMPLETE``WAZUH_REGISTRY_RECOVERED` and `RUNTIME_ACTION_AUTHORIZED` remain forbidden until evidence gates close. Live 110 script sync remains a separate approved live-write gate; do not claim it here. |
| P3 docs / automation contracts | DONE_WITH_OWNER_RESPONSE_PREFLIGHT_V174 | 100% | Workplan, SOP v1.74, post-reboot declaration guard, machine-readable post-reboot readiness summary with Wazuh registry detail fields, post-reboot next-gate dispatch checklist, owner-packet JSON generator, dynamic owner-packet contract guard, post-reboot owner response preflight, owner response placeholder template, one-page post-start quick check v1.14, route retry gate, deploy warmup classification, expanded public route list, StockPlatform freshness gate, StockPlatform cron-source recovery evidence, StockPlatform natural schedule green evidence, 110 orphan Chrome recurrence cleanup evidence, 188 fail-closed startup data recovery gate, 188 host hygiene read-only checklist, 188 PostgreSQL runtime-ready source-of-truth, 188 ACME route/timer hygiene, baseline `stockplatform_system_freshness_ok`, BACKUP-STATUS, LOGBOOK, 120 console/fsck recovery, Gitea backup stale-dump hardening, reboot ledger/version-comparison SOP, escrow evidence audit, 188 nginx Ansible baseline, 110 cold-start detector script, startup judgment layers, GO/NO-GO tree, host recovery cards, explicit Plan B degraded-operation path, machine-readable `plan_b` baseline, readiness-audit Plan B guard, B0-B5 service levels, T+0/T+120 fallback timeline checks, host role / load-balancing assessment, CD `known_hosts` guardrail, `fwupd-refresh.timer` rollback note, K3s filesystem event blocker, AWOOOI backup no-direct-offsite-sync contract, 110/188 Ansible source-of-truth, Gitea self-hosted readiness validation workflow, post-CD no-regression readbacks, stale-vs-active K8s failed Job classification, 110 runaway browser / CI load AIOps exporter + alert + gated remediation PlayBook, Telegram / AI event packet mapping, healthy heartbeat Telegram suppression, MOMO scheduler / current-month detector fix, exporter restore helpers, 110 Docker disk pressure cleanup boundary, notification-noise readback, MOMO import-boundary / Drive-auth fail-closed deploys, product version/readback matrix, and stricter product-data / route retry gates are updated. Declaration guard now machine-checks allowed / forbidden recovery statements: service/data/backup/188 host hygiene green may be declared when live summary says so, while `DR_COMPLETE``WAZUH_REGISTRY_RECOVERED` and `RUNTIME_ACTION_AUTHORIZED` remain forbidden until evidence gates close. Owner response preflight blocks missing files, placeholder templates, secret payloads, credential marker writes, Wazuh active response / re-enroll / restart, host write, and Kali active scan before any evidence can be counted as received or accepted. Live 110 script sync remains a separate approved live-write gate; do not claim it here. |
2026-06-26 12:13 machine-readable summary baseline supersedes the 07:47 / 08:59 gate set: `scripts/reboot-recovery/post-reboot-readiness-summary.sh --no-color` stores delegated logs under `/tmp/awoooi-post-reboot-readiness-20260626-121303` and returns `SERVICE_GREEN=1`, `PRODUCT_DATA_GREEN=1`, `BACKUP_CORE_GREEN=1`, `DR_ESCROW_BLOCKED=1`, `ESCROW_MISSING_COUNT=5`, `HOST_188_SERVICE_GREEN=1`, `HOST_188_HYGIENE_BLOCKED=0`, `HOST_188_CHECK_RC=0`, `HOST_188_RESULT=HOST_188_HYGIENE_GREEN.`, `WAZUH_ROUTE_CODE=200`, `WAZUH_TRANSPORT_COUNT=6`, `WAZUH_COVERAGE_SCOPE=6`, `WAZUH_DIRECT_ACTIVE=2`, `WAZUH_NO_TRANSPORT=1`, `WAZUH_SSH_BLOCKED=3`, `WAZUH_DASHBOARD_API_CONNECTION=pending_or_spinning`, `WAZUH_DASHBOARD_INDEX_OK=3`, `WAZUH_MANAGER_REGISTRY_ACCEPTED=0`, `WAZUH_RUNTIME_GATE=0`, `RUNTIME_ACTION_AUTHORIZED=0`, `OVERALL_DECLARATION=FULL_STACK_GREEN_DR_ESCROW_BLOCKED`, and `NEXT_REQUIRED_GATES=credential_escrow_evidence,wazuh_manager_registry_export`. This is now the preferred first operator/AI-agent entrypoint after reboot because it separates service health from DR and security registry evidence; 188 host hygiene is no longer a next gate unless the live checklist regresses.
@@ -27,6 +27,8 @@
2026-06-26 12:13 owner-packet contract guard baseline: `scripts/reboot-recovery/post-reboot-owner-packet-contract-guard.py --packet-file /tmp/awoooi-post-reboot-owner-packets.json` validates the generated JSON before any owner review intake. It requires the packet gates to equal the live `source.next_required_gates`, preserves `request_sent=0``owner_response_received=0``owner_response_accepted=0``runtime_action_authorized=0``host_write_authorized=0``secret_value_collection_allowed=0``runtime_gate=0`, and rejects missing forbidden payload/action controls for active gates. Current expected success line: `POST_REBOOT_OWNER_PACKET_CONTRACT_GUARD_OK gates=2 request_sent=0 accepted=0 runtime_gate=0`.
2026-06-26 13:01 owner response preflight baseline: `scripts/reboot-recovery/post-reboot-owner-response-preflight.py --no-color` validates future owner responses against the dynamic owner-packet gate set without sending requests, writing markers, reading secrets, or changing runtime. Missing response file must remain `blocked_waiting_owner_response_file`; the placeholder template `docs/templates/post-reboot-next-gate-owner-response.json` must remain `blocked_waiting_owner_response_content` with `received=0`, `accepted=0`, and `runtime_gate=0`. The only acceptable payload class is redacted owner evidence for credential escrow and Wazuh manager registry export; secret values, hash / prefix / suffix, raw Wazuh payload, agent real names, internal IPs, `client.keys`, credential marker write, host write, Wazuh active response / re-enroll / restart, and Kali active scan are rejected.
2026-06-26 08:47 Wazuh registry detail summary baseline: post-reboot readiness summary now emits `WAZUH_COVERAGE_SCOPE`, `WAZUH_DIRECT_ACTIVE`, `WAZUH_NO_TRANSPORT`, `WAZUH_SSH_BLOCKED`, `WAZUH_DASHBOARD_API_CONNECTION`, and `WAZUH_DASHBOARD_INDEX_OK` alongside existing route / transport / registry fields. Current read-only truth is coverage scope `6`, direct active `2`, no transport `1`, SSH blocked `3`, route `200`, transport `6`, Dashboard API `pending_or_spinning`, index OK `3`, manager registry accepted `0`, runtime gate `0`. This is a security evidence blocker, not a reboot service blocker.
2026-06-26 12:13 declaration guard baseline: `scripts/reboot-recovery/post-reboot-declaration-guard.py --no-color` emits `schema_version=awoooi_post_reboot_declaration_guard_v1`, status `allowed_with_boundary_blockers`, allowed declarations including service / product data / backup / 188 host hygiene green for this evidence set, and forbidden declarations `DR_COMPLETE``WAZUH_REGISTRY_RECOVERED``RUNTIME_ACTION_AUTHORIZED`. Proposed false-green declarations are rejected before they can enter LOGBOOK / owner packets / external status updates.

View File

@@ -1,7 +1,7 @@
# AWOOOI 導航與頁面資訊架構整合決策表
> 日期2026-06-25台北時間
> 基線:`gitea/main=5895bd18`deploy `66be2576`,包含 AwoooP Approvals decision handoff rail 與後續 P0 convergence 驗證)
> 基線:`gitea/main=3466fa995944b25ff627046c40f9121a449538f3`
> 目標把左側導航、AwoooP 二層菜單、頁內 tabs 與重複頁面整合成專業 AI 營運產品資訊架構。
> 邊界:本輪先調整導航曝光與 AwoooP contextual rail不刪除路由、不改 API、不開 runtime gate。
@@ -32,7 +32,7 @@
| `/awooop` | 保留為 AwoooP command overview | 不刪 | 改成 Situation Strip + Agent Flow + Risk Matrix + Action Rail。 |
| `/awooop/work-items` | 保留下鑽 | 不刪 | 改成 operator queuemanual item 必須有 SOP rail。 |
| `/awooop/runs` | 保留下鑽 | 不刪 | 改成 execution timelineraw evidence 進 drawer。 |
| `/awooop/approvals` | 保留下鑽 | 不刪 | decision handoff rail 已上線;下一步補 stale approval drawer、owner / rollback / verifier 細節與 legacy alias parity。 |
| `/awooop/approvals` | 保留下鑽 | 不刪 | 改成 decision treestuck approval 要顯示 owner / next action。 |
| `/awooop/contracts` | 保留下鑽 | 不刪 | 合約與 gate 改為 matrix不再純表格。 |
| `/awooop/tenants` | 保留並升級 | 不刪 | 變成全產品 coverage heatmap + topology吸收各產品納管狀態。 |
| `/alerts` | 降級為 AwoooP / Observability 下鑽 | 等 AwoooP incident center 完成後 redirect | 告警不再單獨成主頁;與 incident timeline 串接。 |
@@ -63,126 +63,13 @@
| Command Palette 空查詢只顯示主工作區與快速動作 | 完成 |
| `/knowledge` sidebar 入口改指 `/knowledge-base` | 完成 |
| 子頁 deep link 保留 | 完成 |
| `/awooop/tenants` 新增產品納管作戰圖 | 正式站完成 |
| `/awooop` 新增 AI 自動化真相帶 | 正式站完成 |
| `/awooop/work-items` 新增人工卡點 SOP rail | 正式站完成 |
| `/awooop/approvals` 新增審批決策 handoff rail | 正式站完成 |
## 4.1 2026-06-25 Tenants 作戰圖切片
| 項目 | 結果 |
|---|---|
| Code commit | `c07fefbe feat(web): add product command map to tenants` |
| Deploy marker | `3b552100 chore(cd): deploy c07fefb [skip ci]` |
| Production desktop | `/zh-TW/awooop/tenants?_v=3b552100-tenants-command-map-prod-desktop` |
| Production mobile | `/zh-TW/awooop/tenants?_v=3b552100-tenants-command-map-prod-mobile` |
| API truth | tenant `2`、product surfaces `16`、public routes `31`、candidate repos `10`、in-scope repos `9`、owner accepted `0`、執行閘門 `0`、操作入口 `0` |
| UI 驗證 | 桌機 / 手機都可見 `產品納管作戰圖``所有網站、專案、產品的同一張地圖``產品納管熱力圖``可觀測性``知識與自動化``推版審查` |
| Layout | 桌機 `scrollWidth=1434 clientWidth=1434`;手機 `scrollWidth=384 clientWidth=384`;作戰圖內溢出 `0` |
| 邊界 | 不新增部署、掃描、修復、刪除、重啟或其他危險操作入口;不提升 owner response、執行閘門或 runtime 授權 |
## 4.2 2026-06-25 Tenants 資產表格響應式切片
| 項目 | 結果 |
|---|---|
| Code commit | `d52583d9 feat(web): make tenants asset tables responsive` |
| Deploy marker | `7f44bc3b chore(cd): deploy 20c2c81 [skip ci]`,包含 `d52583d9` |
| Production desktop | `/zh-TW/awooop/tenants?_v=7f44bc3b-tenants-responsive-prod-desktop` |
| Production mobile | `/zh-TW/awooop/tenants?_v=7f44bc3b-tenants-responsive-prod-mobile` |
| Mobile UI | `網站與服務入口``脫敏原始碼範圍` 改為卡片視圖route/source 寬表格在手機隱藏 |
| Layout | 手機 `clientWidth=384``scrollWidth=384``horizontalOverflow=false``overflowing=[]` |
| 邊界 | 不新增任何部署、掃描、修復、刪除、重啟或 runtime execution 入口;仍是 read-only 資產台帳 |
## 4.3 2026-06-25 AwoooP 首屏 AI 自動化真相帶
| 項目 | 結果 |
|---|---|
| Code commit | `092bd376 feat(web): add AwoooP automation truth rail` |
| Deploy marker | `291b6c0c chore(cd): deploy 092bd37 [skip ci]` |
| Production desktop | `/zh-TW/awooop?_v=291b6c0c-truth-rail-prod-desktop` |
| Production mobile | `/zh-TW/awooop?_v=291b6c0c-truth-rail-prod-mobile` |
| 首屏資訊 | `AI 自動化真相`、閉環宣稱、主要卡點、下一步焦點、Runs / 工作項 / 批准下鑽 |
| Layout | desktop `clientWidth=1274 scrollWidth=1274`mobile `clientWidth=384 scrollWidth=384``horizontalOverflow=false` |
| 邊界 | 下鑽是 read-only / operator console 導覽不新增部署、掃描、修復、刪除、重啟、Telegram send 或 runtime execution |
## 4.4 2026-06-25 Work Items operator SOP rail
| 項目 | 結果 |
|---|---|
| Code commit | `aa70835c feat(web): add Work Items operator SOP rail` |
| Deploy marker | `2a9e816a chore(cd): deploy aa70835 [skip ci]` |
| Production desktop | `/zh-TW/awooop/work-items?project_id=awoooi&_v=2a9e816a-work-items-sop-prod-desktop` |
| Production mobile | `/zh-TW/awooop/work-items?project_id=awoooi&_v=2a9e816a-work-items-sop-prod-mobile` |
| 首屏資訊 | 一眼判讀、驗證率、runtime gate、收件 / 證據 / 候選 / 接手 / 驗證流程、四張操作判讀卡 |
| Layout | desktop `clientWidth=1434 scrollWidth=1434`mobile `clientWidth=384 scrollWidth=384``horizontalOverflow=false`rail overflow `0` |
| 邊界 | read-only SOP / operator handoff不新增部署、掃描、修復、刪除、重啟、Telegram send、PlayBook apply、Ansible apply 或 runtime execution |
## 4.5 2026-06-25 Approvals decision handoff rail
| 項目 | 結果 |
|---|---|
| Code commit | `01a8e9d3 feat(web): add approvals decision handoff rail` |
| Deploy marker | `66be2576 chore(cd): deploy bfc78d3 [skip ci]`,包含 `01a8e9d3` |
| Production desktop | `/zh-TW/awooop/approvals?project_id=awoooi&_v=66be2576-approvals-rail-prod-desktop` |
| Production mobile | `/zh-TW/awooop/approvals?project_id=awoooi&_v=66be2576-approvals-rail-prod-mobile` |
| 首屏資訊 | 審批決策 Rail、請求 / 證據 / 決策 / 接手 / 驗證流程、阻塞與人工閘門、AI 證據可用度、接手包與工作項、安全閘門仍關閉 |
| Layout | desktop `clientWidth=1434 scrollWidth=1434`mobile `clientWidth=384 scrollWidth=384``horizontalOverflow=false`rail overflow `0` |
| Error 判讀 | API `/platform/approvals``/approvals/pending``/health``200``無法載入審批資料` 只存在 Next messages scriptvisible error count `0` |
| 邊界 | read-only decision handoff不新增部署、掃描、修復、刪除、重啟、Telegram send、PlayBook apply、Ansible apply、provider switch 或 runtime execution |
## 4.6 2026-06-25 Repair Candidate Draft Ready owner review 狀態模型
| 項目 | 結果 |
|---|---|
| 核心修正 | `node-exporter` / host-service 類 prefilled draft 不再被誤標為 `REPAIR_CANDIDATE_MISSING` |
| 狀態 | `repair_candidate_status=draft_ready_for_owner_review``repair_candidate_draft_ready=true` |
| Webhook | `DRAFT_READY - REPAIR_CANDIDATE_OWNER_REVIEW_REQUIRED`,仍是非執行 approval |
| Telegram | 顯示 `repair_candidate_draft_ready_owner_review``Owner review 處置包` |
| Callback | `ApprovedForOwnerReviewHandoff``manual_handoff_kind=repair_candidate_owner_review` |
| Tests | `py_compile` 通過targeted API tests `21 passed` |
| 邊界 | 不執行主機命令、不發 Telegram、不套用 PlayBook、不跑 Ansible、不開 runtime gate只是讓 owner review 草案成為可追蹤狀態 |
## 4.7 2026-06-25 Runs recurrence Work Item 草案狀態 chip
| 項目 | 結果 |
|---|---|
| UI | `/zh-TW/awooop/runs` recurrence 卡片新增 Work Item status chip |
| 新狀態 | `owner_review_ready``draft_ready``open``blocked``closed``none``unknown` |
| 判讀 | `owner_review_ready` 顯示為「草案待 owner review」不再和「無修復記錄」混淆 |
| 驗證 | JSON parse、i18n mirror、web typecheck、diff check 通過 |
| 邊界 | 只讀狀態顯示不新增執行、重啟、Telegram send 或 PlayBook apply 入口 |
## 4.8 2026-06-25 Ansible check-mode 乾跑 truth-chain 修正
| 項目 | 結果 |
|---|---|
| 核心修正 | `INC-20260625-977E5F` 類 Ansible `check_mode` 乾跑不再被 AwoooP / Telegram 誤標為已執行修復 |
| 判定條件 | `check_mode_total>0``apply_total=0``applied=false``controlled_apply=false``auto_repair_execution_records=0` |
| 新狀態 | `ansible_check_mode_only``dry_run_only_owner_review_required` |
| 下一步 | `owner_review_apply_gate_or_create_verifier_plan` |
| 產品含義 | Runs / Approvals / Telegram 必須把「乾跑完成」與「修復已套用」分成不同階段;後續 Situation Strip 也要把 dry-run、apply、verifier、KM / PlayBook writeback 分層 |
| 驗證 | `py_compile` 通過targeted API / Telegram tests `150 passed`diff check 與 security guards 通過 |
| 正式站 | code `d7b3997b`、deploy marker `420b0b18``INC-20260625-977E5F` production API 讀回 `ansible_check_mode_only``dry_run_completed_no_apply``repair_status=not_executed``ansible_dry_run_only=true` |
| 邊界 | 不執行 Ansible apply、不新增主機命令、不發 Telegram、不開 runtime gate本段只修 truth-chain 與 operator outcome 語意 |
## 4.9 2026-06-25 Runs 資產 ledger 拆出乾跑 / 套用
| 項目 | 結果 |
|---|---|
| 核心修正 | `資產沉澱` 不再把 Ansible candidate、check-mode、apply 混成單一「腳本」完成數 |
| 新顯示 | `乾跑``套用``Verifier` 分開呈現 |
| 判讀 | `ansible_dry_run_only=true``check_mode_total>0 / apply_total=0` 時,乾跑 ready、套用 blocked / `未套用`、Verifier 顯示卡點 |
| IA 含義 | AwoooP 後續 Runs / Work Items / Approvals 必須共享 dry-run -> owner apply gate -> verifier -> KM / PlayBook writeback 的分層語意 |
| 驗證 | JSON parse、i18n leaf diff `0`、web typecheck、diff check 通過 |
| 邊界 | 不新增 action button、不觸發執行、不套用 PlayBook、不開 runtime gate只是把狀態從長文字拆成可掃描的資產格 |
## 5. 下一輪必做
| 優先級 | 工作 | 驗收 |
|---|---|---|
| P0 | AwoooP Runs 共用 Situation Strip / Agent Flow / Action Rail | Recurrence Work Item status chip、Ansible dry-run truth-chain 與資產 ledger 分層已完成;下一步補首屏 Situation Strip讓 Runs 一眼看懂卡點、owner、dry-run、apply、verifier 與下一步 |
| P0 | Repair candidate draft readback 串接 | 後端已拆出 `draft_ready_for_owner_review``ansible_check_mode_only`;下一步 Work Items / KB 顯示草案 ID、owner、rollback、verifier、KM / PlayBook / script / schedule 資產狀態 |
| P0 | Tenants 舊表格 responsive 化 | route / source 已完成卡片化;下一步處理租戶資料表 drawer 與產品 topology drilldown |
| P0 | AwoooP Overview / Runs / Work Items / Approvals 共用 Situation Strip / Agent Flow / Action Rail | desktop / mobile 首屏能一眼看懂卡點與下一步 |
| P0 | Tenants coverage heatmap | 16 產品、31 routes、10 repos、owner / smoke / runtime gate 一張圖看懂 |
| P0 | Observability topology | 主機 / 服務 / 網站 / 告警 / SLO 關聯可視化 |
| P0 | Knowledge / Automation trust ledger | KM、PlayBook、腳本、排程、dry-run、verifier 有統一沉澱面板 |
| P1 | 開始 redirect 舊頁 | 只有在父頁 `?tab=` parity 與 production smoke 通過後執行 |

View File

@@ -41,7 +41,7 @@ resources:
images:
- name: 192.168.0.110:5000/library/api:IMAGE_TAG_PLACEHOLDER
newName: 192.168.0.110:5000/awoooi/api
newTag: 11d23b0b7f7b0cde554676f2bd0204ceb70359f1
newTag: 342bb23cf15b0b54056dfc6de52d80444569911a
- name: 192.168.0.110:5000/library/web:IMAGE_TAG_PLACEHOLDER
newName: 192.168.0.110:5000/awoooi/web
newTag: 11d23b0b7f7b0cde554676f2bd0204ceb70359f1
newTag: 342bb23cf15b0b54056dfc6de52d80444569911a

View File

@@ -0,0 +1,401 @@
#!/usr/bin/env python3
"""Preflight owner responses for post-reboot next gates.
Read-only by design. This script validates an owner response JSON file against
the current post-reboot owner packets. It never sends requests, reads secrets,
writes credential markers, or modifies host/runtime state.
"""
from __future__ import annotations
import argparse
import json
import re
import subprocess
import sys
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[2]
OWNER_PACKET_GENERATOR = (
ROOT / "scripts" / "reboot-recovery" / "post-reboot-next-gate-owner-packets.py"
)
EXPECTED_SCHEMA = "awoooi_post_reboot_next_gate_owner_response_v1"
EXPECTED_OWNER_PACKET_SCHEMA = "awoooi_post_reboot_next_gate_owner_packets_v1"
PLACEHOLDER_VALUES = {
"",
"pending",
"todo",
"tbd",
"n/a",
"na",
"owner_role_here",
"owner_team_here",
"decision_reason_here",
"redacted_evidence_ref_here",
"non_secret_evidence_ref_here",
"registry_export_ref_here",
"followup_owner_here",
"reviewer_here",
}
ESCROW_ITEM_IDS = {
"restic_repository_password",
"offsite_provider_credentials",
"break_glass_admin_credentials",
"dns_registrar_recovery",
"oauth_ai_provider_recovery",
}
EXPECTED_HOST_ALIASES = {
"core-110",
"gateway-188",
"k3s-control-120",
"k3s-control-121",
"security-observer-112",
"dev-workstation-111",
}
FORBIDDEN_BOOLEAN_FIELDS = {
"runtime_action_requested",
"runtime_action_authorized",
"host_write_requested",
"host_write_authorized",
"secret_value_included",
"secret_value_collection_allowed",
"credential_marker_write_requested",
"credential_marker_write_authorized",
"wazuh_active_response_requested",
"wazuh_active_response_authorized",
"agent_reenroll_requested",
"wazuh_restart_requested",
"kali_active_scan_requested",
}
SECRET_VALUE_PATTERNS = [
re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
re.compile(r"\bBearer\s+[A-Za-z0-9._~+/=-]{12,}", re.IGNORECASE),
re.compile(r"\bAuthorization\s*:\s*", re.IGNORECASE),
re.compile(r"\bgh[pousr]_[A-Za-z0-9_]{20,}"),
re.compile(r"\bsk-[A-Za-z0-9]{20,}"),
re.compile(r"\bAIza[0-9A-Za-z_-]{20,}"),
re.compile(r"\b[0-9]{8,10}:[A-Za-z0-9_-]{20,}\b"),
re.compile(r"\b(password|token|secret)\s*[:=]\s*[^,\s]+", re.IGNORECASE),
re.compile(r"\bclient\.keys\b", re.IGNORECASE),
]
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Validate post-reboot owner response evidence without opening runtime gates.",
)
parser.add_argument("--response-file", type=Path, help="Owner response JSON to validate.")
parser.add_argument(
"--owner-packet-file",
type=Path,
help="Use an existing owner packet JSON instead of generating one.",
)
parser.add_argument("--json", action="store_true", help="Print machine-readable JSON.")
parser.add_argument(
"--no-color",
action="store_true",
help="Pass --no-color when generating owner packets.",
)
return parser.parse_args()
def load_json(path: Path) -> dict[str, Any]:
try:
payload = json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise SystemExit(f"response_file_not_found={path}") from exc
except json.JSONDecodeError as exc:
raise SystemExit(f"response_json_invalid={exc}") from exc
if not isinstance(payload, dict):
raise SystemExit("response_json_not_object")
return payload
def generate_owner_packet(no_color: bool) -> dict[str, Any]:
cmd = [str(OWNER_PACKET_GENERATOR)]
if no_color:
cmd.append("--no-color")
completed = subprocess.run(
cmd,
cwd=ROOT,
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
)
if completed.returncode != 0:
raise SystemExit(
"owner_packet_generation_failed "
f"rc={completed.returncode}\n{completed.stdout}"
)
try:
packet = json.loads(completed.stdout)
except json.JSONDecodeError as exc:
raise SystemExit(f"owner_packet_json_invalid={exc}") from exc
if not isinstance(packet, dict):
raise SystemExit("owner_packet_json_not_object")
return packet
def load_owner_packet(args: argparse.Namespace) -> dict[str, Any]:
if args.owner_packet_file:
return load_json(args.owner_packet_file)
return generate_owner_packet(no_color=args.no_color)
def as_list(value: Any) -> list[Any]:
if value is None:
return []
if isinstance(value, list):
return value
return [value]
def normalized(value: Any) -> str:
if value is None:
return ""
return str(value).strip()
def is_placeholder(value: Any) -> bool:
return normalized(value).lower() in PLACEHOLDER_VALUES
def collect_strings(value: Any, path: str = "$") -> list[tuple[str, str]]:
strings: list[tuple[str, str]] = []
if isinstance(value, str):
strings.append((path, value))
elif isinstance(value, dict):
for key, child in value.items():
strings.extend(collect_strings(child, f"{path}.{key}"))
elif isinstance(value, list):
for index, child in enumerate(value):
strings.extend(collect_strings(child, f"{path}[{index}]"))
return strings
def find_forbidden_strings(response: dict[str, Any]) -> list[str]:
failures: list[str] = []
for path, value in collect_strings(response):
if path.endswith(".item_id") and value in ESCROW_ITEM_IDS:
continue
if path.endswith(".gate_id") and value in {
"credential_escrow_evidence",
"wazuh_manager_registry_export",
"host_188_hygiene_maintenance_window",
}:
continue
for pattern in SECRET_VALUE_PATTERNS:
if pattern.search(value):
failures.append(f"forbidden_payload_at={path}")
break
return failures
def find_forbidden_booleans(value: Any, path: str = "$") -> list[str]:
failures: list[str] = []
if isinstance(value, dict):
for key, child in value.items():
child_path = f"{path}.{key}"
if key in FORBIDDEN_BOOLEAN_FIELDS and child is not False:
failures.append(f"{child_path}={child!r}")
failures.extend(find_forbidden_booleans(child, child_path))
elif isinstance(value, list):
for index, child in enumerate(value):
failures.extend(find_forbidden_booleans(child, f"{path}[{index}]"))
return failures
def owner_packet_gate_ids(packet: dict[str, Any]) -> set[str]:
if packet.get("schema_version") != EXPECTED_OWNER_PACKET_SCHEMA:
raise SystemExit(f"owner_packet_schema={packet.get('schema_version')!r}")
return {
str(item.get("packet_id"))
for item in as_list(packet.get("owner_packets"))
if isinstance(item, dict) and item.get("packet_id")
}
def response_by_gate(response: dict[str, Any]) -> dict[str, dict[str, Any]]:
responses = as_list(response.get("responses"))
by_gate: dict[str, dict[str, Any]] = {}
for item in responses:
if not isinstance(item, dict):
continue
gate_id = normalized(item.get("gate_id"))
if gate_id:
by_gate[gate_id] = item
return by_gate
def validate_common(gate_id: str, item: dict[str, Any]) -> list[str]:
failures: list[str] = []
for key in (
"owner_role",
"owner_team",
"decision",
"decision_reason",
"affected_scope",
"followup_owner",
):
if is_placeholder(item.get(key)):
failures.append(f"{gate_id}.{key}_missing")
decision = normalized(item.get("decision")).lower()
if decision not in {"accepted", "rejected", "needs_supplement"}:
failures.append(f"{gate_id}.decision_invalid={decision!r}")
evidence_refs = [
ref for ref in as_list(item.get("redacted_evidence_refs")) if not is_placeholder(ref)
]
if not evidence_refs:
failures.append(f"{gate_id}.redacted_evidence_refs_missing")
return failures
def validate_credential_escrow(item: dict[str, Any]) -> list[str]:
failures: list[str] = []
escrow_items = as_list(item.get("escrow_items"))
seen = {
normalized(entry.get("item_id"))
for entry in escrow_items
if isinstance(entry, dict)
}
missing = sorted(ESCROW_ITEM_IDS - seen)
if missing:
failures.append(f"credential_escrow_evidence.missing_items={missing}")
for entry in escrow_items:
if not isinstance(entry, dict):
failures.append("credential_escrow_evidence.escrow_item_not_object")
continue
item_id = normalized(entry.get("item_id"))
if item_id not in ESCROW_ITEM_IDS:
failures.append(f"credential_escrow_evidence.unknown_item={item_id!r}")
for key in ("non_secret_evidence_ref", "recovery_owner", "reviewer", "last_reviewed_at"):
if is_placeholder(entry.get(key)):
failures.append(f"credential_escrow_evidence.{item_id}.{key}_missing")
if entry.get("contains_secret_value") is not False:
failures.append(f"credential_escrow_evidence.{item_id}.contains_secret_value_not_false")
return failures
def validate_wazuh_registry(item: dict[str, Any]) -> list[str]:
failures: list[str] = []
for key in (
"registry_export_ref",
"registry_time_window",
"dashboard_api_connection_status",
"dashboard_api_version_status",
"reviewer",
):
if is_placeholder(item.get(key)):
failures.append(f"wazuh_manager_registry_export.{key}_missing")
aliases = {normalized(alias) for alias in as_list(item.get("expected_host_aliases"))}
missing_aliases = sorted(EXPECTED_HOST_ALIASES - aliases)
if missing_aliases:
failures.append(f"wazuh_manager_registry_export.missing_aliases={missing_aliases}")
if not isinstance(item.get("manager_registry_count"), int):
failures.append("wazuh_manager_registry_export.manager_registry_count_not_int")
if normalized(item.get("dashboard_api_connection_status")).lower() != "ok":
failures.append("wazuh_manager_registry_export.dashboard_api_connection_not_ok")
if normalized(item.get("dashboard_api_version_status")).lower() != "ok":
failures.append("wazuh_manager_registry_export.dashboard_api_version_not_ok")
return failures
def evaluate(packet: dict[str, Any], response: dict[str, Any] | None) -> dict[str, Any]:
expected_gates = owner_packet_gate_ids(packet)
result: dict[str, Any] = {
"schema_version": "awoooi_post_reboot_owner_response_preflight_v1",
"expected_gate_count": len(expected_gates),
"expected_gates": sorted(expected_gates),
"owner_response_received_count": 0,
"owner_response_accepted_count": 0,
"runtime_gate_count": 0,
"runtime_action_authorized": False,
"host_write_authorized": False,
"secret_value_collection_allowed": False,
"status": "blocked_waiting_owner_response_file",
"blockers": [],
}
if response is None:
result["blockers"] = ["owner_response_file_missing"]
return result
failures: list[str] = []
if response.get("schema_version") != EXPECTED_SCHEMA:
failures.append(f"schema_version={response.get('schema_version')!r}")
failures.extend(find_forbidden_strings(response))
failures.extend(find_forbidden_booleans(response))
by_gate = response_by_gate(response)
gate_ids = set(by_gate)
unknown_gates = sorted(gate_ids - expected_gates)
missing_gates = sorted(expected_gates - gate_ids)
if unknown_gates:
failures.append(f"unknown_gate_ids={unknown_gates}")
if missing_gates:
failures.append(f"missing_gate_responses={missing_gates}")
received = 0
accepted = 0
for gate_id in sorted(expected_gates & gate_ids):
item = by_gate[gate_id]
gate_failures = validate_common(gate_id, item)
if gate_id == "credential_escrow_evidence":
gate_failures.extend(validate_credential_escrow(item))
elif gate_id == "wazuh_manager_registry_export":
gate_failures.extend(validate_wazuh_registry(item))
else:
gate_failures.append(f"{gate_id}.unsupported_for_response_preflight")
if gate_failures:
failures.extend(gate_failures)
else:
received += 1
if normalized(item.get("decision")).lower() == "accepted":
accepted += 1
result["owner_response_received_count"] = received
result["owner_response_accepted_count"] = accepted
result["blockers"] = failures
if failures:
result["status"] = "blocked_waiting_owner_response_content"
elif accepted == len(expected_gates):
result["status"] = "ready_for_independent_reviewer_acceptance"
else:
result["status"] = "blocked_waiting_owner_acceptance"
return result
def main() -> int:
args = parse_args()
packet = load_owner_packet(args)
response = load_json(args.response_file) if args.response_file else None
result = evaluate(packet, response)
if args.json:
print(json.dumps(result, ensure_ascii=False, indent=2, sort_keys=True))
else:
prefix = (
"POST_REBOOT_OWNER_RESPONSE_PREFLIGHT_OK"
if result["status"] == "ready_for_independent_reviewer_acceptance"
else "POST_REBOOT_OWNER_RESPONSE_PREFLIGHT_BLOCKED"
)
print(
f"{prefix} status={result['status']} "
f"expected_gates={result['expected_gate_count']} "
f"received={result['owner_response_received_count']} "
f"accepted={result['owner_response_accepted_count']} "
f"runtime_gate={result['runtime_gate_count']} "
f"blockers={len(result['blockers'])}"
)
return 0
if __name__ == "__main__":
sys.exit(main())