feat(iwooos): add security control coverage board
Some checks failed
Code Review / ai-code-review (push) Successful in 14s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
Some checks failed
Code Review / ai-code-review (push) Successful in 14s
Ansible / Reboot Recovery Contract / validate (push) Has been cancelled
CD Pipeline / build-and-deploy (push) Has been cancelled
CD Pipeline / post-deploy-checks (push) Has been cancelled
CD Pipeline / tests (push) Has been cancelled
This commit is contained in:
@@ -33,7 +33,12 @@ 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'
|
||||
import {
|
||||
apiClient,
|
||||
type IwoooSRuntimeSecurityReadbackResponse,
|
||||
type IwoooSSecurityControlCoverageDomain,
|
||||
type IwoooSSecurityControlCoverageResponse,
|
||||
} from '@/lib/api-client'
|
||||
|
||||
type PostureMetric = {
|
||||
key: string
|
||||
@@ -327,6 +332,13 @@ type RuntimeSecurityReadbackSummaryItem = {
|
||||
tone: 'steady' | 'warn' | 'locked'
|
||||
}
|
||||
|
||||
type SecurityControlCoverageSummaryItem = {
|
||||
key: 'visibleScopes' | 'controlDomains' | 'controlPlane' | 'runtimeAcceptance' | 'ownerAccepted' | 'wazuhAccepted'
|
||||
value: string
|
||||
icon: typeof ShieldCheck
|
||||
tone: 'steady' | 'warn' | 'locked'
|
||||
}
|
||||
|
||||
type SocSiemKaliWazuhIntegrationItem = {
|
||||
key: string
|
||||
check: string
|
||||
@@ -8281,6 +8293,276 @@ function IwoooSRuntimeSecurityReadbackBoard() {
|
||||
)
|
||||
}
|
||||
|
||||
function coverageTone(domain: IwoooSSecurityControlCoverageDomain): 'steady' | 'warn' | 'locked' {
|
||||
if (domain.domain_id === 'wazuh_managed_host_coverage') return 'locked'
|
||||
if (domain.coverage_percent >= 80) return 'steady'
|
||||
if (domain.coverage_percent >= 50) return 'warn'
|
||||
return 'locked'
|
||||
}
|
||||
|
||||
function IwoooSSecurityControlCoverageBoard() {
|
||||
const t = useTranslations('iwooos.securityControlCoverage')
|
||||
const textWrap = { overflowWrap: 'anywhere' as const, wordBreak: 'break-word' as const }
|
||||
const [data, setData] = useState<IwoooSSecurityControlCoverageResponse | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [failed, setFailed] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true
|
||||
|
||||
async function loadCoverage() {
|
||||
setLoading(true)
|
||||
setFailed(false)
|
||||
try {
|
||||
const payload = await apiClient.getIwoooSSecurityControlCoverage()
|
||||
if (mounted) {
|
||||
setData(payload)
|
||||
}
|
||||
} catch {
|
||||
if (mounted) {
|
||||
setData(null)
|
||||
setFailed(true)
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadCoverage()
|
||||
return () => {
|
||||
mounted = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
const summary = data?.summary
|
||||
const statusKey = loading ? 'loading' : failed || !data ? 'failed' : 'ready'
|
||||
const statusTone: 'steady' | 'warn' | 'locked' = loading ? 'warn' : failed || !data ? 'warn' : 'locked'
|
||||
const summaryItems: SecurityControlCoverageSummaryItem[] = [
|
||||
{
|
||||
key: 'visibleScopes',
|
||||
value: summary ? String(summary.visible_scope_unit_count) : '...',
|
||||
icon: Boxes,
|
||||
tone: summary && summary.visible_scope_unit_count > 0 ? 'steady' : 'warn',
|
||||
},
|
||||
{
|
||||
key: 'controlDomains',
|
||||
value: summary ? String(summary.control_domain_count) : '...',
|
||||
icon: Network,
|
||||
tone: summary && summary.control_domain_count > 0 ? 'steady' : 'warn',
|
||||
},
|
||||
{
|
||||
key: 'controlPlane',
|
||||
value: summary ? `${summary.control_plane_visibility_percent}%` : '...',
|
||||
icon: ShieldCheck,
|
||||
tone: summary && summary.control_plane_visibility_percent >= 70 ? 'steady' : 'warn',
|
||||
},
|
||||
{
|
||||
key: 'runtimeAcceptance',
|
||||
value: summary ? `${summary.actual_runtime_acceptance_percent}%` : '...',
|
||||
icon: Lock,
|
||||
tone: 'locked',
|
||||
},
|
||||
{
|
||||
key: 'ownerAccepted',
|
||||
value: summary ? String(summary.owner_response_accepted_count) : '...',
|
||||
icon: ClipboardCheck,
|
||||
tone: 'locked',
|
||||
},
|
||||
{
|
||||
key: 'wazuhAccepted',
|
||||
value: summary ? String(summary.wazuh_manager_registry_accepted_count) : '...',
|
||||
icon: Radar,
|
||||
tone: 'locked',
|
||||
},
|
||||
]
|
||||
const domains = data?.domains ?? []
|
||||
const p0Actions = data?.p0_next_actions ?? []
|
||||
|
||||
return (
|
||||
<section
|
||||
style={{ marginBottom: 14, maxWidth: '100%', overflow: 'hidden' }}
|
||||
data-testid="iwooos-security-control-coverage-board"
|
||||
>
|
||||
<div style={{ ...band, padding: 16, background: '#fcfbf7', borderColor: '#ded7c2' }}>
|
||||
<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: '#6a5730', fontSize: 12, fontWeight: 700 }}>
|
||||
<Boxes size={17} color="#80652b" />
|
||||
{t('eyebrow')}
|
||||
</div>
|
||||
<h2 style={{ fontSize: 17, margin: '8px 0 0', color: '#141413' }}>{t('title')}</h2>
|
||||
<p style={{ fontSize: 12, color: '#5d5036', margin: '6px 0 0', lineHeight: 1.55, ...textWrap }}>
|
||||
{t('subtitle')}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style={{ border: '0.5px solid #e0d8bf', borderRadius: 8, padding: 12, background: '#fff', minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
|
||||
<span style={{ fontSize: 11, color: '#75684b', 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: '#5d5036', 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 #e0d8bf', borderRadius: 8, padding: 12, background: '#fff', minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
||||
<span style={{ fontSize: 11, color: '#75684b', ...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: '#5d5036', 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%, 240px), 1fr))',
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{domains.length === 0 ? (
|
||||
<div style={{ border: '0.5px solid #e0d8bf', borderRadius: 8, background: '#fff', padding: 13, color: '#5d5036', fontSize: 12 }}>
|
||||
{t('emptyDomains')}
|
||||
</div>
|
||||
) : domains.map(domain => {
|
||||
const tone = coverageTone(domain)
|
||||
return (
|
||||
<div
|
||||
key={domain.domain_id}
|
||||
style={{
|
||||
border: `0.5px solid ${tone === 'steady' ? '#bddfc9' : tone === 'warn' ? '#e6d1a6' : '#dad7ce'}`,
|
||||
borderRadius: 8,
|
||||
background: tone === 'warn' ? '#fffaf1' : tone === 'locked' ? '#f8f7f3' : '#fff',
|
||||
padding: 13,
|
||||
display: 'grid',
|
||||
gap: 10,
|
||||
minWidth: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, alignItems: 'flex-start' }}>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ fontSize: 12, color: toneColors[tone], fontWeight: 700, ...textWrap }}>{domain.label}</div>
|
||||
<div style={{ marginTop: 5, fontSize: 11, color: '#5d5036', lineHeight: 1.45, ...textWrap }}>
|
||||
{t(`domainBody.${domain.domain_id}` as never)}
|
||||
</div>
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: toneColors[tone], fontWeight: 700 }}>{domain.coverage_percent}%</span>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 6 }}>
|
||||
{([
|
||||
['scope', domain.scope_count],
|
||||
['blocked', domain.blocked_count],
|
||||
['accepted', domain.accepted_count],
|
||||
] as const).map(([key, value]) => (
|
||||
<div key={key} style={{ border: '0.5px solid #e7debf', borderRadius: 8, background: '#fff', padding: 8, minWidth: 0 }}>
|
||||
<div style={{ fontSize: 10, color: '#75684b', fontWeight: 700 }}>{t(`domainMetric.${key}` as never)}</div>
|
||||
<div style={{ marginTop: 5, fontSize: 14, color: key === 'accepted' ? toneColors.locked : '#141413', fontWeight: 700 }}>
|
||||
{value}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div style={{ border: '0.5px solid #e7debf', borderRadius: 8, padding: 8, background: '#fff', minWidth: 0 }}>
|
||||
<div style={{ fontSize: 10, color: '#75684b', fontWeight: 700 }}>{t('domainStatusLabel')}</div>
|
||||
<div style={{ marginTop: 5, fontSize: 11, color: '#141413', fontWeight: 700, ...textWrap }}>
|
||||
{t(`domainStatus.${domain.status}` as never)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: 14 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, color: '#6a5730', fontSize: 12, fontWeight: 700 }}>
|
||||
<ListChecks size={16} color="#80652b" />
|
||||
{t('p0Title')}
|
||||
</div>
|
||||
<p style={{ fontSize: 11, color: '#5d5036', lineHeight: 1.5, margin: '6px 0 10px', ...textWrap }}>
|
||||
{t('p0Intro')}
|
||||
</p>
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(min(100%, 240px), 1fr))', gap: 8 }}>
|
||||
{p0Actions.map(action => (
|
||||
<div key={action.priority} style={{ border: '0.5px solid #e0d8bf', borderRadius: 8, background: '#fff', padding: 11, minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 7 }}>
|
||||
<span style={{ fontSize: 11, color: '#8a5b1f', fontWeight: 800 }}>{action.priority}</span>
|
||||
<span style={{ fontSize: 12, color: '#141413', fontWeight: 700, ...textWrap }}>{action.title}</span>
|
||||
</div>
|
||||
<p style={{ fontSize: 11, color: '#5d5036', lineHeight: 1.45, margin: '7px 0 0', ...textWrap }}>
|
||||
{t('requiredEvidenceLabel')}:{action.required_evidence}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details
|
||||
data-testid="iwooos-security-control-coverage-boundaries"
|
||||
style={{
|
||||
marginTop: 12,
|
||||
border: '0.5px solid #e0d8bf',
|
||||
borderRadius: 8,
|
||||
background: '#fff',
|
||||
padding: '8px 10px',
|
||||
}}
|
||||
>
|
||||
<summary style={{ cursor: 'pointer', fontSize: 12, fontWeight: 700, color: '#6a5730' }}>
|
||||
{t('boundaryTitle')}
|
||||
</summary>
|
||||
<div style={{ marginTop: 8, display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(230px, 1fr))', gap: 6 }}>
|
||||
{(data?.no_false_green_rules ?? []).map(item => (
|
||||
<code
|
||||
key={item}
|
||||
style={{
|
||||
border: '0.5px solid #e0d8bf',
|
||||
borderRadius: 8,
|
||||
padding: '6px 8px',
|
||||
color: '#5d5036',
|
||||
fontSize: 11,
|
||||
lineHeight: 1.4,
|
||||
background: '#fffaf1',
|
||||
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'
|
||||
@@ -22178,6 +22460,7 @@ export default function IwoooSPage({ params }: { params: { locale: string } }) {
|
||||
<IwoooSRolloutRiskReadOnlyBoard />
|
||||
<IwoooSWazuhIntrusionReadbackBoard />
|
||||
<IwoooSRuntimeSecurityReadbackBoard />
|
||||
<IwoooSSecurityControlCoverageBoard />
|
||||
<IwoooSWazuhLiveRouteReadbackBoard />
|
||||
<IwoooSWazuhReleaseGateBoard />
|
||||
<IwoooSWazuhOwnerEvidencePreflightBoard />
|
||||
|
||||
@@ -144,6 +144,73 @@ export interface IwoooSRuntimeSecurityReadbackResponse {
|
||||
no_false_green_rules: string[]
|
||||
}
|
||||
|
||||
export interface IwoooSSecurityControlCoverageDomain {
|
||||
domain_id:
|
||||
| 'high_value_asset_control'
|
||||
| 'host_service_runtime'
|
||||
| 'monitoring_alerting_observability'
|
||||
| 'ssh_firewall_network_access'
|
||||
| 'awoooi_runtime_surfaces'
|
||||
| 'wazuh_managed_host_coverage'
|
||||
| 'agent_bounty_protocol'
|
||||
| 'ai_agent_automation'
|
||||
label: string
|
||||
priority: string
|
||||
coverage_percent: number
|
||||
scope_count: number
|
||||
write_capable_count: number
|
||||
accepted_count: number
|
||||
blocked_count: number
|
||||
status: string
|
||||
next_gate: string
|
||||
source_refs: string[]
|
||||
}
|
||||
|
||||
export interface IwoooSSecurityControlCoverageResponse {
|
||||
schema_version: 'iwooos_security_control_coverage_v1'
|
||||
status: string
|
||||
mode: string
|
||||
summary: {
|
||||
source_snapshot_count: number
|
||||
control_domain_count: number
|
||||
visible_scope_unit_count: number
|
||||
write_capable_scope_count: number
|
||||
blocked_scope_count: number
|
||||
accepted_scope_count: number
|
||||
control_plane_visibility_percent: number
|
||||
actual_runtime_acceptance_percent: number
|
||||
runtime_gate_count: number
|
||||
owner_response_received_count: number
|
||||
owner_response_accepted_count: number
|
||||
live_evidence_accepted_count: number
|
||||
wazuh_manager_registry_accepted_count: number
|
||||
active_scan_authorized_count: number
|
||||
active_response_authorized_count: number
|
||||
telegram_send_authorized_count: number
|
||||
host_write_authorized_count: number
|
||||
secret_value_collected_count: number
|
||||
agent_bounty_runtime_gate_open_count: number
|
||||
ai_agent_runtime_write_gate_open_count: number
|
||||
asset_group_count: number
|
||||
host_service_surface_count: number
|
||||
monitoring_surface_count: number
|
||||
ssh_network_surface_count: number
|
||||
runtime_surface_count: number
|
||||
wazuh_expected_host_scope_count: number
|
||||
agent_bounty_product_surface_count: number
|
||||
ai_agent_asset_count: number
|
||||
all_scope_runtime_controlled: boolean
|
||||
}
|
||||
domains: IwoooSSecurityControlCoverageDomain[]
|
||||
p0_next_actions: Array<{
|
||||
priority: string
|
||||
title: string
|
||||
required_evidence: string
|
||||
}>
|
||||
no_false_green_rules: string[]
|
||||
source_refs: string[]
|
||||
}
|
||||
|
||||
async function handleResponse<T>(response: Response): Promise<T> {
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}))
|
||||
@@ -182,6 +249,11 @@ export const apiClient = {
|
||||
return handleResponse<IwoooSRuntimeSecurityReadbackResponse>(res)
|
||||
},
|
||||
|
||||
async getIwoooSSecurityControlCoverage() {
|
||||
const res = await fetch(`${API_BASE_URL}/iwooos/security-control-coverage`, { cache: 'no-store' })
|
||||
return handleResponse<IwoooSSecurityControlCoverageResponse>(res)
|
||||
},
|
||||
|
||||
// Agent
|
||||
async getAgentStatus() {
|
||||
const res = await fetch(`${API_BASE_URL}/agent/status`)
|
||||
|
||||
Reference in New Issue
Block a user