From 935ceda4e0c0bd163effcbc2ae52a6018430ceab Mon Sep 17 00:00:00 2001 From: Ligbox Spec Hub Date: Thu, 25 Jun 2026 11:55:31 +0000 Subject: [PATCH] =?UTF-8?q?Console:=20barra=20de=20alertas=20clic=C3=A1vel?= =?UTF-8?q?=20com=20drill-down=20no=20Discover.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cada contador de severidade liga a /discover?bucket= com agente, regra, domínio e chamado; contagens calculadas a partir dos eventos Wazuh 24h. Co-authored-by: Cursor --- .../frontend/src/components/AlertBar.jsx | 39 +++++ .../deploy/frontend/src/lib/events.js | 32 ++++ .../deploy/frontend/src/lib/severity.js | 44 ++++++ .../deploy/frontend/src/mock.js | 23 ++- .../deploy/frontend/src/theme.css | 37 +++++ .../deploy/frontend/src/views/ConsoleHome.jsx | 40 ++--- .../deploy/frontend/src/views/Discover.jsx | 137 ++++++++++++++---- 7 files changed, 298 insertions(+), 54 deletions(-) create mode 100644 specs/019-ops-console-active-operations/deploy/frontend/src/components/AlertBar.jsx create mode 100644 specs/019-ops-console-active-operations/deploy/frontend/src/lib/events.js create mode 100644 specs/019-ops-console-active-operations/deploy/frontend/src/lib/severity.js diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/components/AlertBar.jsx b/specs/019-ops-console-active-operations/deploy/frontend/src/components/AlertBar.jsx new file mode 100644 index 0000000..b475903 --- /dev/null +++ b/specs/019-ops-console-active-operations/deploy/frontend/src/components/AlertBar.jsx @@ -0,0 +1,39 @@ +import { Link } from 'react-router-dom' +import { SEVERITY_BUCKETS } from '../lib/severity' + +export default function AlertBar({ counts, linkable = true }) { + return ( +
+ {SEVERITY_BUCKETS.map(({ key, label, hint, className }) => { + const val = counts[key] ?? 0 + const inner = ( + <> + {val} + {label} + {hint} + {linkable && val > 0 ? ( + Ver alertas → + ) : null} + + ) + if (linkable && val > 0) { + return ( + + {inner} + + ) + } + return ( +
+ {inner} +
+ ) + })} +
+ ) +} diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/lib/events.js b/specs/019-ops-console-active-operations/deploy/frontend/src/lib/events.js new file mode 100644 index 0000000..2083b0a --- /dev/null +++ b/specs/019-ops-console-active-operations/deploy/frontend/src/lib/events.js @@ -0,0 +1,32 @@ +import { MOCK_DISCOVER_EVENTS } from '../mock' +import { apiGet } from './api' + +function normalizeApiEvent(row) { + const payload = row.payload || {} + const data = payload.data || {} + const level = row.severity ?? data.level ?? payload.level + return { + id: row.id, + source: row.source || 'wazuh', + event_type: row.event_type, + domain: row.domain || payload.domain || data.agent || '—', + severity: level != null ? Number(level) : null, + summary: data.description || payload.summary || row.event_type, + agent: data.agent || payload.agent || '—', + rule_id: data.rule_id || payload.rule_id, + chamado_public_id: payload.chamado_public_id || null, + created_at: row.created_at, + wazuh_link: 'https://wazuh.itecnologys.com/', + } +} + +export async function loadDiscoverEvents() { + try { + const data = await apiGet('/api/v1/webhooks/events?source=wazuh') + const rows = (data.events || []).map(normalizeApiEvent) + if (rows.length) return { events: rows, fromApi: true } + } catch { + /* sem login JWT — mock */ + } + return { events: MOCK_DISCOVER_EVENTS, fromApi: false } +} diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/lib/severity.js b/specs/019-ops-console-active-operations/deploy/frontend/src/lib/severity.js new file mode 100644 index 0000000..8f2709b --- /dev/null +++ b/specs/019-ops-console-active-operations/deploy/frontend/src/lib/severity.js @@ -0,0 +1,44 @@ +/** Buckets Wazuh — mesma escala da home Console */ + +export const SEVERITY_BUCKETS = [ + { key: 'critical', label: 'Critical severity', hint: 'Rule level 15 or higher', className: 'wz-sev-critical', min: 15, max: 99 }, + { key: 'high', label: 'High severity', hint: 'Rule level 12 to 14', className: 'wz-sev-high', min: 12, max: 14 }, + { key: 'medium', label: 'Medium severity', hint: 'Rule level 7 to 11', className: 'wz-sev-med', min: 7, max: 11 }, + { key: 'low', label: 'Low severity', hint: 'Rule level 0 to 6', className: 'wz-sev-low', min: 0, max: 6 }, +] + +export function bucketForLevel(level) { + const n = Number(level) + if (Number.isNaN(n)) return null + return SEVERITY_BUCKETS.find((b) => n >= b.min && n <= b.max)?.key ?? null +} + +export function aggregateWazuh24h(events) { + const counts = Object.fromEntries(SEVERITY_BUCKETS.map((b) => [b.key, 0])) + const now = Date.now() + const windowMs = 24 * 60 * 60 * 1000 + for (const ev of events || []) { + if (ev.source !== 'wazuh') continue + const ts = ev.created_at ? new Date(ev.created_at).getTime() : now + if (now - ts > windowMs) continue + const bucket = bucketForLevel(ev.severity) + if (bucket) counts[bucket] += 1 + } + return counts +} + +export function filterByBucket(events, bucketKey) { + if (!bucketKey) return events + const now = Date.now() + const windowMs = 24 * 60 * 60 * 1000 + return (events || []).filter((ev) => { + if (bucketForLevel(ev.severity) !== bucketKey) return false + if (ev.source !== 'wazuh') return false + const ts = ev.created_at ? new Date(ev.created_at).getTime() : now + return now - ts <= windowMs + }) +} + +export function bucketLabel(bucketKey) { + return SEVERITY_BUCKETS.find((b) => b.key === bucketKey)?.label ?? bucketKey +} diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/mock.js b/specs/019-ops-console-active-operations/deploy/frontend/src/mock.js index 3073ba8..5f86b9f 100644 --- a/specs/019-ops-console-active-operations/deploy/frontend/src/mock.js +++ b/specs/019-ops-console-active-operations/deploy/frontend/src/mock.js @@ -31,8 +31,27 @@ export const MOCK_CHAMADOS = [ ] export const MOCK_EVENTS = [ - { id: 901, chamado_public_id: 'CH-2026-00042', source: 'wazuh', event_type: 'wazuh.alert', domain: 'myvexx.com', severity: 12, summary: 'SSH brute force' }, - { id: 902, chamado_public_id: 'CH-2026-00042', source: 'onboard', event_type: 'onboarding.failed', domain: 'myvexx.com', severity: null, summary: 'DNS timeout' }, + { id: 901, chamado_public_id: 'CH-2026-00042', source: 'wazuh', event_type: 'wazuh.alert', domain: 'myvexx.com', severity: 12, summary: 'SSH brute force', agent: 'vm112-mail', rule_id: '5712', created_at: new Date(Date.now() - 3600000).toISOString() }, + { id: 902, chamado_public_id: 'CH-2026-00042', source: 'onboard', event_type: 'onboarding.failed', domain: 'myvexx.com', severity: null, summary: 'DNS timeout', agent: '—', rule_id: null, created_at: new Date(Date.now() - 3000000).toISOString() }, +] + +/** Alertas Wazuh últimas 24h — alimentam barra + Discover filtrável */ +export const MOCK_DISCOVER_EVENTS = [ + { id: 1001, source: 'wazuh', event_type: 'wazuh.alert', domain: 'myvexx.com', severity: 9, summary: 'Integrity checksum changed', agent: 'vm112-mail', rule_id: '550', chamado_public_id: null, created_at: new Date(Date.now() - 2 * 3600000).toISOString() }, + { id: 1002, source: 'wazuh', event_type: 'wazuh.alert', domain: 'ligbox.com.br', severity: 8, summary: 'Multiple authentication failures', agent: 'vm123-finance', rule_id: '5710', chamado_public_id: null, created_at: new Date(Date.now() - 5 * 3600000).toISOString() }, + { id: 1003, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 5, summary: 'sshd: authentication failure', agent: 'vm122-desk', rule_id: '5712', chamado_public_id: null, created_at: new Date(Date.now() - 1 * 3600000).toISOString() }, + { id: 1004, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 4, summary: 'PAM: User login failed', agent: 'vm122-desk', rule_id: '5503', chamado_public_id: null, created_at: new Date(Date.now() - 3 * 3600000).toISOString() }, + { id: 1005, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 3, summary: 'New Yum package installed', agent: 'vm104-wazuh', rule_id: '23504', chamado_public_id: null, created_at: new Date(Date.now() - 6 * 3600000).toISOString() }, + { id: 1006, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 3, summary: 'Listened ports status changed', agent: 'traefik-proxy', rule_id: '533', chamado_public_id: null, created_at: new Date(Date.now() - 8 * 3600000).toISOString() }, + { id: 1007, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 2, summary: 'syslog: cron session opened', agent: 'vm112-mail', rule_id: '2833', chamado_public_id: null, created_at: new Date(Date.now() - 10 * 3600000).toISOString() }, + { id: 1008, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 2, summary: 'File added to the system', agent: 'vm123-finance', rule_id: '554', chamado_public_id: null, created_at: new Date(Date.now() - 12 * 3600000).toISOString() }, + { id: 1009, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 2, summary: 'Dpkg (Debian Package) changed', agent: 'vm122-desk', rule_id: '2902', chamado_public_id: null, created_at: new Date(Date.now() - 14 * 3600000).toISOString() }, + { id: 1010, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 1, summary: 'Login session opened', agent: 'vm104-wazuh', rule_id: '5501', chamado_public_id: null, created_at: new Date(Date.now() - 16 * 3600000).toISOString() }, + { id: 1011, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 1, summary: 'PAM: Login session closed', agent: 'traefik-proxy', rule_id: '5502', chamado_public_id: null, created_at: new Date(Date.now() - 18 * 3600000).toISOString() }, + { id: 1012, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 1, summary: 'sshd: Accepted publickey', agent: 'vm112-mail', rule_id: '5715', chamado_public_id: null, created_at: new Date(Date.now() - 20 * 3600000).toISOString() }, + { id: 1013, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 0, summary: 'Agent started', agent: 'vm122-desk', rule_id: '503', chamado_public_id: null, created_at: new Date(Date.now() - 22 * 3600000).toISOString() }, + { id: 1014, source: 'wazuh', event_type: 'wazuh.alert', domain: '—', severity: 0, summary: 'Agent keepalive', agent: 'vm123-finance', rule_id: '504', chamado_public_id: null, created_at: new Date(Date.now() - 23 * 3600000).toISOString() }, + { id: 902, chamado_public_id: 'CH-2026-00042', source: 'onboard', event_type: 'onboarding.failed', domain: 'myvexx.com', severity: null, summary: 'DNS timeout', agent: '—', rule_id: null, created_at: new Date(Date.now() - 3000000).toISOString() }, ] export const MOCK_TENANTS = [ diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/theme.css b/specs/019-ops-console-active-operations/deploy/frontend/src/theme.css index e46c55c..fcd3342 100644 --- a/specs/019-ops-console-active-operations/deploy/frontend/src/theme.css +++ b/specs/019-ops-console-active-operations/deploy/frontend/src/theme.css @@ -143,6 +143,43 @@ a:hover { text-decoration: underline; } text-align: center; } +.wz-alert-stat--link { + text-decoration: none; + color: inherit; + transition: background 0.15s; + cursor: pointer; +} + +.wz-alert-stat--link:hover { + background: var(--lb-accent-soft); + text-decoration: none; +} + +.wz-alert-hint { + display: block; + font-size: 0.65rem; + color: var(--lb-text-muted); + text-transform: none; + margin-top: 0.2rem; +} + +.wz-alert-cta { + display: block; + font-size: 0.72rem; + color: var(--lb-accent); + margin-top: 0.45rem; + font-weight: 600; +} + +.discover-filter-banner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 1rem; +} + .wz-alert-stat:last-child { border-right: none; } .wz-alert-stat strong { diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/views/ConsoleHome.jsx b/specs/019-ops-console-active-operations/deploy/frontend/src/views/ConsoleHome.jsx index 85e7c9b..08fa5aa 100644 --- a/specs/019-ops-console-active-operations/deploy/frontend/src/views/ConsoleHome.jsx +++ b/specs/019-ops-console-active-operations/deploy/frontend/src/views/ConsoleHome.jsx @@ -1,27 +1,11 @@ import { useEffect, useState } from 'react' import { Link } from 'react-router-dom' +import AlertBar from '../components/AlertBar' +import { loadDiscoverEvents } from '../lib/events' +import { aggregateWazuh24h } from '../lib/severity' import { MOCK_AUDIT_TENANTS, MOCK_CHAMADOS, MOCK_TENANTS } from '../mock' import { apiGet } from '../lib/api' -function AlertBar() { - return ( -
- {[ - ['0', 'Critical severity', 'wz-sev-critical', 'Rule level 15 or higher'], - ['0', 'High severity', 'wz-sev-high', 'Rule level 12 to 14'], - ['2', 'Medium severity', 'wz-sev-med', 'Rule level 7 to 11'], - ['12', 'Low severity', 'wz-sev-low', 'Rule level 0 to 6'], - ].map(([val, label, cls, hint]) => ( -
- {val} - {label} - {hint} -
- ))} -
- ) -} - function FeatureSection({ title, children }) { return (
@@ -103,12 +87,18 @@ function AuditTenantCards({ auditTenants }) { export default function ConsoleHome() { const [infra, setInfra] = useState(MOCK_TENANTS) const [audit, setAudit] = useState(MOCK_AUDIT_TENANTS) + const [alertCounts, setAlertCounts] = useState({ critical: 0, high: 0, medium: 0, low: 0 }) const [usingMock, setUsingMock] = useState(true) useEffect(() => { let cancelled = false ;(async () => { try { + const { events, fromApi } = await loadDiscoverEvents() + if (!cancelled) { + setAlertCounts(aggregateWazuh24h(events)) + if (fromApi) setUsingMock(false) + } const [tData, aData] = await Promise.all([ apiGet('/api/v1/tenants').catch(() => null), apiGet('/api/v1/audit/overview').catch(() => null), @@ -133,11 +123,11 @@ export default function ConsoleHome() { <>

Console

- Operações unificadas — chamados, discover, tenants de infraestrutura e saúde de domínios num só lugar. - {usingMock ? ' (dados de demonstração — login API em breve)' : ''} + Operações unificadas — clique nos números de alerta para ver quem gerou cada evento (agente, regra, domínio). + {usingMock ? ' Dados de demonstração até login API.' : ''}

- + diff --git a/specs/019-ops-console-active-operations/deploy/frontend/src/views/Discover.jsx b/specs/019-ops-console-active-operations/deploy/frontend/src/views/Discover.jsx index 18ceb29..145bb30 100644 --- a/specs/019-ops-console-active-operations/deploy/frontend/src/views/Discover.jsx +++ b/specs/019-ops-console-active-operations/deploy/frontend/src/views/Discover.jsx @@ -1,36 +1,119 @@ -import { Link } from 'react-router-dom' -import { MOCK_EVENTS } from '../mock' +import { useEffect, useMemo, useState } from 'react' +import { Link, useSearchParams } from 'react-router-dom' +import { loadDiscoverEvents } from '../lib/events' +import { bucketLabel, filterByBucket } from '../lib/severity' + +function fmtWhen(iso) { + if (!iso) return '—' + try { + return new Date(iso).toLocaleString('pt-BR', { dateStyle: 'short', timeStyle: 'short' }) + } catch { + return iso + } +} export default function Discover() { + const [searchParams, setSearchParams] = useSearchParams() + const bucket = searchParams.get('bucket') + const [events, setEvents] = useState([]) + const [fromApi, setFromApi] = useState(false) + const [loading, setLoading] = useState(true) + + useEffect(() => { + let cancelled = false + ;(async () => { + setLoading(true) + const { events: rows, fromApi: api } = await loadDiscoverEvents() + if (!cancelled) { + setEvents(rows) + setFromApi(api) + setLoading(false) + } + })() + return () => { cancelled = true } + }, []) + + const filtered = useMemo(() => { + if (!bucket) return events + return filterByBucket(events, bucket) + }, [events, bucket]) + + const clearFilter = () => setSearchParams({}) + return ( <> +

← Console

Discover

-

Feed unificado — clique num evento para abrir o hub CH-* (≤ 2 cliques).

-
- - - - - - - - {MOCK_EVENTS.map((e) => ( - - - - - - - + {bucket ? ( +
+
+ Filtro activo: {bucketLabel(bucket)} — {filtered.length} alerta(s) nas últimas 24h +
+ +
+ ) : ( +

+ Feed unificado Wazuh + onboard. Cada linha mostra agente, regra e ligação ao chamado. + {fromApi ? '' : ' (demonstração — login API para dados live)'} +

+ )} + + {loading ? ( +

Carregando eventos…

+ ) : filtered.length === 0 ? ( +
+

Nenhum alerta neste filtro nas últimas 24 horas.

+
+ ) : ( +
+
IDOrigemEventoDomínioSevChamado
{e.id}{e.source}{e.event_type}{e.domain}{e.severity ?? '—'} - {e.chamado_public_id ? ( - {e.chamado_public_id} - ) : '—'} -
+ + + + + + + + + + - ))} - -
QuandoSevAgenteRegraDescriçãoDomínioChamadoWazuh
-
+ + + {filtered.map((e) => ( + + {fmtWhen(e.created_at)} + + {e.severity != null ? ( + = 15 ? 'critical' : e.severity >= 12 ? 'high' : e.severity >= 7 ? 'medium' : 'low'}`}> + L{e.severity} + + ) : '—'} + + {e.agent || '—'} + {e.rule_id ? {e.rule_id} : '—'} + {e.summary || e.event_type} + {e.domain || '—'} + + {e.chamado_public_id ? ( + {e.chamado_public_id} + ) : '—'} + + + {e.source === 'wazuh' ? ( + + Abrir + + ) : ( + {e.source} + )} + + + ))} + + + + )} ) }