feat(iwooos): show Wazuh route readback status
This commit is contained in:
@@ -19003,6 +19003,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"wazuhLiveRouteReadback": {
|
||||
"eyebrow": "Wazuh 正式路由只讀讀回",
|
||||
"title": "正式站必須直接顯示 Wazuh 只讀路由狀態",
|
||||
"subtitle": "這張卡只讀 `/api/iwooos/wazuh`,把未部署、未啟用、代理清單為空、低於預期與可讀狀態轉成操作員可理解的繁中訊號;不顯示代理身分、內網位址、原始內容或機密。",
|
||||
"statusDetail": "此狀態只代表正式路由讀回結果;退化或未部署時不能顯示綠燈,也不授權主機操作、主動回應、掃描、重啟或機密變更。",
|
||||
"boundaryTitle": "正式路由讀回邊界",
|
||||
"boundaryIntro": "以下鍵值只用於避免誤判:正式路由未部署、唯讀查詢未啟用或代理清單退化時,都不能顯示綠燈。",
|
||||
"status": {
|
||||
"loading": "讀取正式路由中",
|
||||
"predeploy": "正式路由尚未部署",
|
||||
"disabled": "唯讀查詢尚未啟用",
|
||||
"available": "只讀中繼資料可讀",
|
||||
"registryEmpty": "代理清單為空,禁止顯示綠燈",
|
||||
"belowExpected": "代理數低於預期,禁止顯示綠燈",
|
||||
"misconfigured": "伺服端環境尚未通過",
|
||||
"unavailable": "正式路由讀回不可用"
|
||||
},
|
||||
"metrics": {
|
||||
"route": {
|
||||
"label": "路由狀態",
|
||||
"detail": "正式站 HTTP 讀回碼。"
|
||||
},
|
||||
"readonly": {
|
||||
"label": "唯讀查詢",
|
||||
"detail": "只顯示是否啟用,不顯示連線資訊。"
|
||||
},
|
||||
"agents": {
|
||||
"label": "代理總數",
|
||||
"detail": "只顯示 aggregate,不列代理清單。"
|
||||
},
|
||||
"runtimeGate": {
|
||||
"label": "執行閘門",
|
||||
"detail": "主動回應與主機寫入仍維持 0。"
|
||||
}
|
||||
},
|
||||
"details": {
|
||||
"active": "在線代理",
|
||||
"disconnected": "離線代理",
|
||||
"pending": "等待代理",
|
||||
"expectedMin": "預期下限"
|
||||
}
|
||||
},
|
||||
"wazuhLiveMetadataEnvGate": {
|
||||
"eyebrow": "Wazuh 即時中繼資料環境閘門",
|
||||
"title": "Wazuh 查詢要等正式路由、負責人與機密中繼資料都過關",
|
||||
|
||||
@@ -19003,6 +19003,48 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"wazuhLiveRouteReadback": {
|
||||
"eyebrow": "Wazuh 正式路由只讀讀回",
|
||||
"title": "正式站必須直接顯示 Wazuh 只讀路由狀態",
|
||||
"subtitle": "這張卡只讀 `/api/iwooos/wazuh`,把未部署、未啟用、代理清單為空、低於預期與可讀狀態轉成操作員可理解的繁中訊號;不顯示代理身分、內網位址、原始內容或機密。",
|
||||
"statusDetail": "此狀態只代表正式路由讀回結果;退化或未部署時不能顯示綠燈,也不授權主機操作、主動回應、掃描、重啟或機密變更。",
|
||||
"boundaryTitle": "正式路由讀回邊界",
|
||||
"boundaryIntro": "以下鍵值只用於避免誤判:正式路由未部署、唯讀查詢未啟用或代理清單退化時,都不能顯示綠燈。",
|
||||
"status": {
|
||||
"loading": "讀取正式路由中",
|
||||
"predeploy": "正式路由尚未部署",
|
||||
"disabled": "唯讀查詢尚未啟用",
|
||||
"available": "只讀中繼資料可讀",
|
||||
"registryEmpty": "代理清單為空,禁止顯示綠燈",
|
||||
"belowExpected": "代理數低於預期,禁止顯示綠燈",
|
||||
"misconfigured": "伺服端環境尚未通過",
|
||||
"unavailable": "正式路由讀回不可用"
|
||||
},
|
||||
"metrics": {
|
||||
"route": {
|
||||
"label": "路由狀態",
|
||||
"detail": "正式站 HTTP 讀回碼。"
|
||||
},
|
||||
"readonly": {
|
||||
"label": "唯讀查詢",
|
||||
"detail": "只顯示是否啟用,不顯示連線資訊。"
|
||||
},
|
||||
"agents": {
|
||||
"label": "代理總數",
|
||||
"detail": "只顯示 aggregate,不列代理清單。"
|
||||
},
|
||||
"runtimeGate": {
|
||||
"label": "執行閘門",
|
||||
"detail": "主動回應與主機寫入仍維持 0。"
|
||||
}
|
||||
},
|
||||
"details": {
|
||||
"active": "在線代理",
|
||||
"disconnected": "離線代理",
|
||||
"pending": "等待代理",
|
||||
"expectedMin": "預期下限"
|
||||
}
|
||||
},
|
||||
"wazuhLiveMetadataEnvGate": {
|
||||
"eyebrow": "Wazuh 即時中繼資料環境閘門",
|
||||
"title": "Wazuh 查詢要等正式路由、負責人與機密中繼資料都過關",
|
||||
|
||||
@@ -279,6 +279,22 @@ type WazuhLiveMetadataEnvGateItem = {
|
||||
tone: 'steady' | 'warn' | 'locked'
|
||||
}
|
||||
|
||||
type WazuhReadonlyStatusResponse = {
|
||||
status?: string
|
||||
configured?: boolean
|
||||
summary?: {
|
||||
readonly_api_enabled_count?: number
|
||||
agent_total?: number
|
||||
agent_active?: number
|
||||
agent_disconnected?: number
|
||||
agent_pending?: number
|
||||
expected_min_agent_count?: number
|
||||
agent_registry_empty_count?: number
|
||||
agent_below_expected_minimum_count?: number
|
||||
runtime_gate_count?: number
|
||||
}
|
||||
}
|
||||
|
||||
type SocSiemKaliWazuhIntegrationItem = {
|
||||
key: string
|
||||
check: string
|
||||
@@ -7721,6 +7737,185 @@ function IwoooSWazuhIntrusionReadbackBoard() {
|
||||
)
|
||||
}
|
||||
|
||||
function wazuhReadonlyStatusKey(httpStatus: number | null, data: WazuhReadonlyStatusResponse | null) {
|
||||
if (httpStatus === 404) return 'predeploy'
|
||||
if (!data) return 'unavailable'
|
||||
if (data.status === 'readonly_metadata_available') return 'available'
|
||||
if (data.status === 'wazuh_agent_registry_empty') return 'registryEmpty'
|
||||
if (data.status === 'wazuh_agent_registry_below_expected') return 'belowExpected'
|
||||
if (data.status === 'disabled_waiting_iwooos_wazuh_owner_gate') return 'disabled'
|
||||
if (data.status === 'misconfigured_missing_server_side_wazuh_env') return 'misconfigured'
|
||||
if (data.status === 'wazuh_readonly_metadata_unavailable') return 'unavailable'
|
||||
return 'unavailable'
|
||||
}
|
||||
|
||||
function IwoooSWazuhLiveRouteReadbackBoard() {
|
||||
const t = useTranslations('iwooos.wazuhLiveRouteReadback')
|
||||
const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const }
|
||||
const [data, setData] = useState<WazuhReadonlyStatusResponse | null>(null)
|
||||
const [httpStatus, setHttpStatus] = useState<number | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
|
||||
async function loadReadback() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await fetch('/api/iwooos/wazuh', {
|
||||
headers: { Accept: 'application/json' },
|
||||
cache: 'no-store',
|
||||
signal: controller.signal,
|
||||
})
|
||||
setHttpStatus(response.status)
|
||||
if (response.ok) {
|
||||
const payload = await response.json() as WazuhReadonlyStatusResponse
|
||||
setData(payload)
|
||||
} else {
|
||||
setData(null)
|
||||
}
|
||||
} catch {
|
||||
if (!controller.signal.aborted) {
|
||||
setHttpStatus(null)
|
||||
setData(null)
|
||||
}
|
||||
} finally {
|
||||
if (!controller.signal.aborted) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadReadback()
|
||||
return () => controller.abort()
|
||||
}, [])
|
||||
|
||||
const summary = data?.summary ?? {}
|
||||
const statusKey = loading ? 'loading' : wazuhReadonlyStatusKey(httpStatus, data)
|
||||
const statusTone = statusKey === 'available' ? 'steady' : statusKey === 'predeploy' || statusKey === 'disabled' ? 'locked' : 'warn'
|
||||
const routeValue = loading ? '...' : httpStatus === 404 ? '404' : httpStatus ? String(httpStatus) : '0'
|
||||
const readonlyValue = summary.readonly_api_enabled_count === 1 ? '1' : '0'
|
||||
const agentTotal = typeof summary.agent_total === 'number' ? String(summary.agent_total) : '0'
|
||||
const runtimeGate = typeof summary.runtime_gate_count === 'number' ? String(summary.runtime_gate_count) : '0'
|
||||
const metrics = [
|
||||
{ key: 'route', value: routeValue, icon: Route, tone: statusTone },
|
||||
{ key: 'readonly', value: readonlyValue, icon: Radar, tone: readonlyValue === '1' ? 'warn' : 'locked' },
|
||||
{ key: 'agents', value: agentTotal, icon: Server, tone: summary.agent_registry_empty_count === 1 || summary.agent_below_expected_minimum_count === 1 ? 'warn' : 'locked' },
|
||||
{ key: 'runtimeGate', value: runtimeGate, icon: Lock, tone: 'locked' },
|
||||
] as const
|
||||
const detailRows = [
|
||||
{ key: 'active', value: typeof summary.agent_active === 'number' ? String(summary.agent_active) : '0' },
|
||||
{ key: 'disconnected', value: typeof summary.agent_disconnected === 'number' ? String(summary.agent_disconnected) : '0' },
|
||||
{ key: 'pending', value: typeof summary.agent_pending === 'number' ? String(summary.agent_pending) : '0' },
|
||||
{ key: 'expectedMin', value: typeof summary.expected_min_agent_count === 'number' ? String(summary.expected_min_agent_count) : '0' },
|
||||
] as const
|
||||
|
||||
return (
|
||||
<section
|
||||
style={{ marginBottom: 14, maxWidth: '100%', overflow: 'hidden' }}
|
||||
data-testid="iwooos-wazuh-live-route-readback-board"
|
||||
>
|
||||
<div style={{ ...band, padding: 16, background: '#fbfaf6', borderColor: '#d8d3c7' }}>
|
||||
<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: '#5f5b52', fontSize: 12, fontWeight: 700 }}>
|
||||
<Route size={17} color="#6f6a5f" />
|
||||
{t('eyebrow')}
|
||||
</div>
|
||||
<h2 style={{ fontSize: 17, margin: '8px 0 0', color: '#141413' }}>{t('title')}</h2>
|
||||
<p style={{ fontSize: 12, color: '#5f5b52', margin: '6px 0 0', lineHeight: 1.55, ...textWrap }}>
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(126px, 1fr))', gap: 8 }}>
|
||||
{metrics.map(item => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<div key={item.key} style={{ border: '0.5px solid #ded8c8', borderRadius: 8, padding: 12, background: '#fff' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<span style={{ fontSize: 11, color: '#77736a' }}>{t(`metrics.${item.key}.label` as never)}</span>
|
||||
<Icon size={16} color={toneColors[item.tone]} />
|
||||
</div>
|
||||
<div style={{ fontSize: 19, fontWeight: 700, color: toneColors[item.tone], lineHeight: 1.1, marginTop: 8, ...textWrap }}>
|
||||
{item.value}
|
||||
</div>
|
||||
<p style={{ fontSize: 11, color: '#5f5b52', lineHeight: 1.45, margin: '8px 0 0', ...textWrap }}>
|
||||
{t(`metrics.${item.key}.detail` as never)}
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 12, display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 180px), 1fr))', gap: 8 }}>
|
||||
<div style={{ border: '0.5px solid #ded8c8', borderRadius: 8, background: '#fff', padding: 12, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 700, color: toneColors[statusTone], ...textWrap }}>
|
||||
{t(`status.${statusKey}` as never)}
|
||||
</div>
|
||||
<p style={{ fontSize: 11, lineHeight: 1.5, color: '#5f5b52', margin: '8px 0 0', ...textWrap }}>
|
||||
{t('statusDetail')}
|
||||
</p>
|
||||
</div>
|
||||
{detailRows.map(row => (
|
||||
<div key={row.key} style={{ border: '0.5px solid #ded8c8', borderRadius: 8, background: '#fff', padding: 12, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 11, color: '#77736a' }}>{t(`details.${row.key}` as never)}</div>
|
||||
<div style={{ marginTop: 8, fontSize: 18, fontWeight: 700, color: '#141413' }}>{row.value}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<details
|
||||
data-testid="iwooos-wazuh-live-route-readback-boundaries"
|
||||
style={{
|
||||
marginTop: 12,
|
||||
border: '0.5px solid #ded8c8',
|
||||
borderRadius: 8,
|
||||
background: '#fff',
|
||||
padding: '8px 10px',
|
||||
}}
|
||||
>
|
||||
<summary style={{ cursor: 'pointer', fontSize: 12, fontWeight: 700, color: '#5f5b52' }}>
|
||||
{t('boundaryTitle')}
|
||||
</summary>
|
||||
<p style={{ fontSize: 11, color: '#5f5b52', lineHeight: 1.5, margin: '8px 0', ...textWrap }}>
|
||||
{t('boundaryIntro')}
|
||||
</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 6 }}>
|
||||
{[
|
||||
`production_route_http_status=${httpStatus ?? 0}`,
|
||||
`readonly_api_enabled_count=${readonlyValue}`,
|
||||
`agent_registry_empty_count=${summary.agent_registry_empty_count ?? 0}`,
|
||||
`agent_below_expected_minimum_count=${summary.agent_below_expected_minimum_count ?? 0}`,
|
||||
`runtime_gate_count=${runtimeGate}`,
|
||||
'agent_identity_public_display_allowed=false',
|
||||
'internal_ip_public_display_allowed=false',
|
||||
'raw_wazuh_payload_storage_allowed=false',
|
||||
].map(item => (
|
||||
<code
|
||||
key={item}
|
||||
style={{
|
||||
border: '0.5px solid #ded8c8',
|
||||
borderRadius: 8,
|
||||
padding: '6px 8px',
|
||||
color: '#5f5b52',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.4,
|
||||
background: '#fbfaf6',
|
||||
overflowWrap: 'anywhere',
|
||||
}}
|
||||
>
|
||||
{item}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function IwoooSWazuhLiveMetadataEnvGateBoard() {
|
||||
const t = useTranslations('iwooos.wazuhLiveMetadataEnvGate')
|
||||
const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const }
|
||||
@@ -20775,6 +20970,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) {
|
||||
<IwoooSAgentBountySecurityOnboardingBoard />
|
||||
<IwoooSRolloutRiskReadOnlyBoard />
|
||||
<IwoooSWazuhIntrusionReadbackBoard />
|
||||
<IwoooSWazuhLiveRouteReadbackBoard />
|
||||
<IwoooSWazuhLiveMetadataEnvGateBoard />
|
||||
<IwoooSSocSiemKaliWazuhIntegrationBoard />
|
||||
<IwoooSSecurityAssetControlLedgerBoard />
|
||||
|
||||
@@ -1,3 +1,29 @@
|
||||
## 2026-06-25|IwoooS Wazuh 正式路由只讀讀回前台
|
||||
|
||||
**背景**:IwoooS 前台已有 Wazuh 入侵回讀與環境閘門,但缺少直接顯示 `/api/iwooos/wazuh` 目前讀回狀態的卡片;operator 仍可能把「Wazuh 已建置」誤解成「正式路由已部署、agent registry 已驗收」。
|
||||
|
||||
**完成**:
|
||||
- `/zh-TW/iwooos` 新增 `Wazuh 正式路由只讀讀回` 卡片。
|
||||
- 卡片只讀 `/api/iwooos/wazuh`,把未部署、未啟用、只讀可用、代理清單為空、低於預期、伺服端環境未通過與不可用狀態轉成繁中訊號。
|
||||
- 僅顯示 HTTP 讀回碼、唯讀查詢啟用計數、aggregate 代理數、執行閘門與退化計數;不顯示 agent alias、內網位址、raw payload、帳密或完整 response。
|
||||
- 可見文案明確標示退化或未部署時不能顯示綠燈,且不授權主機操作、主動回應、掃描、重啟或機密變更。
|
||||
|
||||
**驗證**:
|
||||
- `node -e "JSON.parse(...)"`:`zh-TW` / `en` JSON 解析通過。
|
||||
- `cmp -s apps/web/messages/zh-TW.json apps/web/messages/en.json`:通過。
|
||||
- `pnpm --filter @awoooi/web typecheck`:通過。
|
||||
- `python3 scripts/security/security-mirror-progress-guard.py --root .`:通過。
|
||||
- `python3 scripts/security/wazuh-readonly-route-boundary-guard.py --root .`:通過。
|
||||
- 本地 Chrome smoke `/zh-TW/iwooos` desktop `1440x1100` 與 mobile `390x1200`:`data-testid=iwooos-wazuh-live-route-readback-board` 可見、`horizontalOverflow=0`、可見文案包含 `不能顯示綠燈`、未出現工作視窗片段、直白主機別名、Wazuh env key、token 或內網位址。
|
||||
|
||||
**完成度同步**:
|
||||
- IwoooS Wazuh 正式路由讀回前台:`100%` source-side。
|
||||
- IwoooS Wazuh 前台可讀性:`91% -> 94%` source-side。
|
||||
- Wazuh live route production readback:仍為 `0%`,正式站仍需部署後驗收。
|
||||
- Wazuh manager registry live accepted、Dashboard stored API 修復、owner response、active response / host write:仍維持 `0%`。
|
||||
|
||||
**邊界**:本輪沒有查 live Wazuh manager、沒有 SSH、沒有讀 secret、沒有送 Telegram、沒有部署、沒有修改 Wazuh / Docker / Nginx / firewall,也沒有 active response。
|
||||
|
||||
## 2026-06-25|Wazuh release handoff 分層狀態校正
|
||||
|
||||
**背景**:Wazuh release handoff 仍把「Gitea push」寫成 `0%`,容易讓另一個工作視窗誤會 feature branch 尚未推送。實際狀態是 feature branch 已推送,但 formal main release、production deploy 與 live metadata env enable 仍為 `0%`。
|
||||
|
||||
Reference in New Issue
Block a user