Compare commits
8 Commits
codex/ai-a
...
codex/iwoo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e359886af | ||
|
|
88630ab7fa | ||
|
|
4ad579a09c | ||
|
|
342bb23cf1 | ||
|
|
35ab800ff7 | ||
|
|
03e5557f91 | ||
|
|
84791ab5d4 | ||
|
|
ec8377e732 |
@@ -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
|
||||
|
||||
322
apps/api/src/services/iwooos_runtime_security_readback.py
Normal file
322
apps/api/src/services/iwooos_runtime_security_readback.py
Normal 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}")
|
||||
@@ -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"
|
||||
|
||||
67
apps/api/tests/test_iwooos_runtime_security_readback.py
Normal file
67
apps/api/tests/test_iwooos_runtime_security_readback.py
Normal 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
|
||||
@@ -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 還要補 readback;index pattern 通過不能宣稱 Wazuh 全綠。"
|
||||
},
|
||||
"kali_intake": {
|
||||
"title": "資安觀測節點 intake",
|
||||
"body": "先收 health、scope、工具版本與 normalized finding envelope;active 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 只讀路由狀態",
|
||||
|
||||
@@ -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 還要補 readback;index pattern 通過不能宣稱 Wazuh 全綠。"
|
||||
},
|
||||
"kali_intake": {
|
||||
"title": "資安觀測節點 intake",
|
||||
"body": "先收 health、scope、工具版本與 normalized finding envelope;active 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 只讀路由狀態",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -1,3 +1,34 @@
|
||||
## 2026-06-26|IwoooS 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 份已提交資安 snapshot:S4.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-26|AwoooP 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、未取消 CI;12: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 preflight:v1.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`。
|
||||
|
||||
@@ -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-closed;preflight 通過也只表示可進入獨立 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。
|
||||
|
||||
@@ -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 checklist;Wazuh 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` 不可宣稱 complete;Wazuh 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 checklist;Wazuh 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` 不可宣稱 complete;Wazuh 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
|
||||
|
||||
98
docs/templates/post-reboot-next-gate-owner-response.json
vendored
Normal file
98
docs/templates/post-reboot-next-gate-owner-response.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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 queue;manual item 必須有 SOP rail。 |
|
||||
| `/awooop/runs` | 保留下鑽 | 不刪 | 改成 execution timeline;raw evidence 進 drawer。 |
|
||||
| `/awooop/approvals` | 保留下鑽 | 不刪 | decision handoff rail 已上線;下一步補 stale approval drawer、owner / rollback / verifier 細節與 legacy alias parity。 |
|
||||
| `/awooop/approvals` | 保留下鑽 | 不刪 | 改成 decision tree;stuck 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 script,visible 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 通過後執行 |
|
||||
|
||||
@@ -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
|
||||
|
||||
401
scripts/reboot-recovery/post-reboot-owner-response-preflight.py
Executable file
401
scripts/reboot-recovery/post-reboot-owner-response-preflight.py
Executable 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())
|
||||
Reference in New Issue
Block a user