465 lines
15 KiB
Markdown
465 lines
15 KiB
Markdown
# Implementation Plan: Ligbox Ops Console — Operação Activa (019)
|
||
|
||
**Branch:** `019-ops-console-active-operations`
|
||
**Date:** 2026-06-16
|
||
**Spec:** [spec.md](./spec.md)
|
||
|
||
---
|
||
|
||
## Summary
|
||
|
||
Implementar a **Ligbox Ops Console** — navegação investigativa tipo Wazuh com **`CH-*` como hub central** (chamado único, assumir, runbooks, aprovações humano + agentic, Assist Opção A).
|
||
|
||
**IA:** [design/navigation-ia.md](./design/navigation-ia.md)
|
||
|
||
| Camada | Host | Deploy |
|
||
|--------|------|--------|
|
||
| **Console UI** | **VM123** | **Docker Compose** (obrigatório — VM já tem serviços) |
|
||
| **API motor** | VM122 | Docker Compose existente (`ligbox-ops-platform`) |
|
||
| **Detecção** | VM104 | Wazuh manager (inalterado) |
|
||
| **Eventos onboard** | VM112 | systemd wizard |
|
||
|
||
**URL pública:** `https://console.ligbox.com.br` → Traefik CT114 → VM123:8100
|
||
**API:** `https://api.ops.ligbox.com.br` → VM122:8080
|
||
|
||
---
|
||
|
||
## Technical Context
|
||
|
||
**Language/Version**
|
||
|
||
| Componente | Stack |
|
||
|------------|-------|
|
||
| Console UI | React 18 + Vite 6 + TypeScript (recomendado) ou React JS |
|
||
| API extensões | Python 3.11+ FastAPI (VM122 `api/app/`) |
|
||
| VM112 Assist | Python FastAPI — novo endpoint `ops-status` |
|
||
| Deploy VM123 | Docker Compose v2, `nginx:alpine` |
|
||
|
||
**Primary Dependencies**
|
||
|
||
- Console: React Router (`/chamados/:publicId` = hub), TanStack Query, Zustand (auth)
|
||
- API: FastAPI, sqlite3, httpx, websockets (Fase 6)
|
||
- Agentic: gateway LLM existente (A6/A7 — Fase 6)
|
||
|
||
**Storage (VM122)**
|
||
|
||
Novas tabelas SQLite (ver Phase 1). MVP mantém SQLite; migração Postgres fora de escopo.
|
||
|
||
**Testing**
|
||
|
||
- `scripts/verify-console-health.sh` — VM123 Docker + Traefik
|
||
- `scripts/verify-chamado-unico.sh` — agregação eventos → `CH-*`
|
||
- `scripts/verify-runbook-r1.sh` — fila aprovação + execução mock
|
||
|
||
**Target Platform**
|
||
|
||
- VM123 LAN `10.10.10.123` (confirmar IP no Proxmox)
|
||
- Bind UI: `127.0.0.1:8100` ou `10.10.10.123:8100`
|
||
- Comunicação VM123 → VM122: LAN HTTPS via Traefik ou direct `http://10.10.10.122:8080` (dev)
|
||
|
||
**Performance Goals**
|
||
|
||
- Console first paint < 2s (gzip brotli via nginx)
|
||
- Discover feed < 800ms (paginação 50)
|
||
- Chamado detail + timeline < 500ms
|
||
- Poll Assist Opção A: 30s (MVP)
|
||
|
||
**Constraints**
|
||
|
||
- **VM123:** zero alteração em serviços/containers pré-existentes
|
||
- **Sem fork** `wazuh-dashboard` / OpenSearch
|
||
- **Sem replay** browser (Opção A apenas)
|
||
- Runbooks R2/R3: audit log obrigatório
|
||
- JWT Spec 003 em todas as rotas Console
|
||
|
||
---
|
||
|
||
## Constitution Check
|
||
|
||
| Princípio | Status |
|
||
|-----------|--------|
|
||
| IV. Mail vs Ops | ✅ PASS — UI na VM123; motor VM122; VM112 só webhooks + ops-status read-only |
|
||
| V. Security baseline | ✅ PASS — fail2ban host VM123; container non-root; secrets em `.env` |
|
||
| VII. Spec-Driven | ✅ PASS |
|
||
| IX. YAGNI | ✅ PASS — SQLite; poll antes de WebSocket |
|
||
|
||
---
|
||
|
||
## Project Structure
|
||
|
||
```text
|
||
specs/019-ops-console-active-operations/
|
||
├── spec.md
|
||
├── plan.md
|
||
├── tasks.md
|
||
├── deploy/ # Copiar para /opt/ligbox-ops-console/ na VM123
|
||
│ ├── docker-compose.yml
|
||
│ ├── .env.example
|
||
│ ├── nginx/
|
||
│ │ └── default.conf
|
||
│ └── scripts/
|
||
│ ├── preflight-vm123.sh # Inventário portas antes do deploy
|
||
│ └── deploy-console.sh
|
||
├── contracts/
|
||
│ └── chamados-api.md
|
||
└── design/
|
||
└── tokens.css # Referência paleta Wazuh-like
|
||
|
||
# Repositório produção (a criar)
|
||
/opt/ligbox-ops-console/ # VM123
|
||
├── docker-compose.yml # ← cópia de deploy/
|
||
├── .env
|
||
├── frontend/ # SPA React
|
||
│ ├── Dockerfile
|
||
│ ├── src/
|
||
│ │ ├── views/ # Overview, Discover, ChamadosList, ChamadoHub
|
||
│ │ ├── components/ # Timeline, Observables, AssistPanel, DrillDownLink
|
||
│ │ ├── api/ # client → api.ops.ligbox.com.br
|
||
│ │ └── theme/ # tokens Wazuh-like
|
||
│ └── dist/ # build → nginx volume
|
||
|
||
/opt/ligbox-ops-platform/ # VM122 (existente)
|
||
├── api/app/
|
||
│ ├── main.py # + rotas /chamados, /discover, /aprovacoes
|
||
│ ├── chamados.py # agregação CH-*, estados
|
||
│ ├── runbooks/ # executor + políticas R0-R3
|
||
│ └── agentic/ # propostas (Fase 6)
|
||
└── worker/
|
||
└── chamado_aggregator.py # merge eventos → chamado_id
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 0: Pré-deploy VM123 (obrigatório)
|
||
|
||
**Objectivo:** garantir que Docker Console não conflita com serviços existentes.
|
||
|
||
```bash
|
||
# Na VM123 — antes de qualquer deploy
|
||
./scripts/preflight-vm123.sh
|
||
```
|
||
|
||
Checklist:
|
||
|
||
| # | Verificação | Comando | Critério |
|
||
|---|-------------|---------|----------|
|
||
| 1 | Porta 8100 livre | `ss -tlnp \| grep :8100` | Vazio |
|
||
| 2 | Docker activo | `docker info` | OK |
|
||
| 3 | Containers existentes | `docker ps` | Listar — **não parar** |
|
||
| 4 | Disco | `df -h /opt` | ≥ 5 GB livres |
|
||
| 5 | IP LAN | `ip -4 addr show` | Confirmar `10.10.10.123` |
|
||
| 6 | Acesso VM122 | `curl -s -o /dev/null -w '%{http_code}' http://10.10.10.122:8080/api/health` | 200 |
|
||
|
||
Documentar resultado em ticket interno ou `docs/network/VM123_INVENTARIO.md` (criar após 1º preflight).
|
||
|
||
---
|
||
|
||
## Phase 1: Data Model — Chamado único (VM122)
|
||
|
||
```sql
|
||
-- Migração 019_001_chamados.sql
|
||
|
||
CREATE TABLE chamados (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
public_id TEXT NOT NULL UNIQUE, -- CH-2026-00042
|
||
status TEXT NOT NULL DEFAULT 'novo',
|
||
assignee TEXT,
|
||
domain TEXT,
|
||
session_id TEXT,
|
||
wizard_step TEXT,
|
||
wizard_step_at TEXT,
|
||
sources TEXT NOT NULL DEFAULT '[]', -- JSON array
|
||
max_severity INTEGER,
|
||
title TEXT,
|
||
created_at TEXT NOT NULL,
|
||
updated_at TEXT NOT NULL
|
||
);
|
||
|
||
CREATE INDEX idx_chamados_domain ON chamados(domain);
|
||
CREATE INDEX idx_chamados_session ON chamados(session_id);
|
||
CREATE INDEX idx_chamados_status ON chamados(status);
|
||
|
||
CREATE TABLE chamado_eventos (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
chamado_id INTEGER NOT NULL REFERENCES chamados(id),
|
||
webhook_event_id INTEGER REFERENCES webhook_events(id),
|
||
event_type TEXT NOT NULL,
|
||
source TEXT NOT NULL,
|
||
payload TEXT,
|
||
created_at TEXT NOT NULL
|
||
);
|
||
|
||
CREATE TABLE runbook_execucoes (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
chamado_id INTEGER NOT NULL REFERENCES chamados(id),
|
||
runbook_code TEXT NOT NULL,
|
||
nivel TEXT NOT NULL, -- R0, R1, R2, R3
|
||
status TEXT NOT NULL, -- pendente, aprovado, executando, ok, falhou, rejeitado
|
||
proposta TEXT, -- JSON agentic
|
||
actor TEXT, -- user | agent | system
|
||
resultado TEXT,
|
||
created_at TEXT NOT NULL,
|
||
executed_at TEXT
|
||
);
|
||
|
||
CREATE TABLE aprovacoes (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
runbook_exec_id INTEGER NOT NULL REFERENCES runbook_execucoes(id),
|
||
aprovador TEXT,
|
||
decisao TEXT, -- aprovado | rejeitado
|
||
nota TEXT,
|
||
created_at TEXT NOT NULL
|
||
);
|
||
|
||
CREATE TABLE chamado_observables (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
chamado_id INTEGER NOT NULL REFERENCES chamados(id),
|
||
tipo TEXT NOT NULL, -- domain, session_id, agent, rule_id, ip, email
|
||
valor TEXT NOT NULL,
|
||
fonte TEXT NOT NULL, -- wazuh, onboard, manual, extractor
|
||
created_at TEXT NOT NULL,
|
||
UNIQUE(chamado_id, tipo, valor)
|
||
);
|
||
|
||
CREATE TABLE chamado_notas (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
chamado_id INTEGER NOT NULL REFERENCES chamados(id),
|
||
autor TEXT NOT NULL,
|
||
texto TEXT NOT NULL,
|
||
created_at TEXT NOT NULL
|
||
);
|
||
|
||
-- Denormalizar chamado_id em webhook_events (opcional fase 1b)
|
||
ALTER TABLE webhook_events ADD COLUMN chamado_id INTEGER REFERENCES chamados(id);
|
||
```
|
||
|
||
### Lógica de agregação (`chamado_aggregator.py`)
|
||
|
||
1. Novo `webhook_event` inserido → worker busca chamado aberto:
|
||
- `session_id` + `domain` match
|
||
- senão `domain` + status ∉ (`fechado`, `resolvido`) + created < 72h
|
||
- senão `wazuh.agent.id` + domain/IP
|
||
2. Se não encontrar → `INSERT chamados` + `public_id` sequencial
|
||
3. `INSERT chamado_eventos` + update `chamados.sources`, `max_severity`, `updated_at`
|
||
4. Eventos `account.created` / `wazuh.alert` L≥10 podem forçar criação imediata
|
||
5. **Extractor** popula `chamado_observables` a partir de cada payload (domain, agent, rule_id, …)
|
||
|
||
---
|
||
|
||
## Phase 2b: Hub API (VM122)
|
||
|
||
| Endpoint | Hub section |
|
||
|----------|-------------|
|
||
| `GET .../chamados/{id}` | `timeline`, `observables`, `assist`, `infra`, `links` |
|
||
| `POST .../notas` | append timeline |
|
||
| `POST .../eventos/anexar` | Discover → hub |
|
||
| `GET .../links/wazuh` | deep link VM104 |
|
||
|
||
Helper `build_wazuh_deep_link(agent, from, to, rule_id)` — URL dashboard VM104.
|
||
|
||
---
|
||
|
||
## Phase 2: API VM122
|
||
|
||
Ver [contracts/chamados-api.md](./contracts/chamados-api.md).
|
||
|
||
| Prioridade | Endpoint | Fase |
|
||
|------------|----------|------|
|
||
| P1 | `GET /api/v1/chamados` | F2 |
|
||
| P1 | `GET /api/v1/chamados/{public_id}` | F2 |
|
||
| P1 | `POST /api/v1/chamados/{public_id}/assumir` | F2 |
|
||
| P1 | `PATCH /api/v1/chamados/{public_id}` | F2 |
|
||
| P1 | `GET /api/v1/discover` | F3 |
|
||
| P1 | `POST /api/v1/chamados/{id}/runbooks/{code}/executar` | F4 |
|
||
| P1 | `GET /api/v1/aprovacoes` | F4 |
|
||
| P1 | `POST /api/v1/aprovacoes/{id}/aprovar` | F4 |
|
||
| P2 | `WS /api/v1/chamados/{public_id}/live` | F6 |
|
||
|
||
**Compatibilidade:** `GET /api/v1/desk/tickets` mantém-se 90 dias; resposta inclui `chamado_public_id` quando mapeado.
|
||
|
||
---
|
||
|
||
## Phase 3: Console UI — VM123 Docker
|
||
|
||
### Stack deploy (referência)
|
||
|
||
Ficheiros em [deploy/](./deploy/):
|
||
|
||
```bash
|
||
# VM123
|
||
rsync -av specs/019-ops-console-active-operations/deploy/ root@10.10.10.123:/opt/ligbox-ops-console/
|
||
ssh root@10.10.10.123 'cd /opt/ligbox-ops-console && cp .env.example .env && nano .env'
|
||
ssh root@10.10.10.123 'cd /opt/ligbox-ops-console && ./scripts/deploy-console.sh'
|
||
```
|
||
|
||
**Importante:** `docker compose up -d` só afecta serviços definidos neste compose — **não** `docker compose down` global na VM.
|
||
|
||
### Traefik CT114 (label router)
|
||
|
||
```yaml
|
||
# Adicionar em dynamic.yml ou labels do service discovery
|
||
http:
|
||
routers:
|
||
ligbox-ops-console:
|
||
rule: Host(`console.ligbox.com.br`)
|
||
entryPoints: [websecure]
|
||
service: ligbox-ops-console
|
||
tls:
|
||
certResolver: letsencrypt
|
||
services:
|
||
ligbox-ops-console:
|
||
loadBalancer:
|
||
servers:
|
||
- url: http://10.10.10.123:8100
|
||
```
|
||
|
||
### Views MVP (F3)
|
||
|
||
| View | Rota | Conteúdo |
|
||
|------|------|----------|
|
||
| **Overview** | `/` | Stats, funil, alertas → link hub |
|
||
| **Discover** | `/discover` | Feed + filtros → **abrir hub** |
|
||
| **Chamados** | `/chamados` | Lista fila trabalho |
|
||
| **Hub** | `/chamados/:publicId` | **Investigação central** — timeline + tabs |
|
||
| **Tenants** | `/tenants` | Agentes/VMs; drill-down → hub |
|
||
| **Aprovações** | `/aprovacoes` | Fila; cada item → hub origem |
|
||
|
||
### Componentes hub (prioridade implementação)
|
||
|
||
1. `ChamadoHubLayout` — shell 2 colunas
|
||
2. `InvestigationTimeline` — eventos + notas + runbooks
|
||
3. `ObservablesPanel` — entidades clicáveis (`DrillDownLink`)
|
||
4. `AssistPanel` — Opção A
|
||
5. `InfraScorecardEmbed` — proxy 009
|
||
6. `WazuhDeepLinkButton` — novo tab VM104
|
||
7. `DiscoverAttachBar` — seleccionar eventos → anexar ao hub activo
|
||
|
||
### Tema Wazuh-like
|
||
|
||
- Ficheiro referência: [design/tokens.css](./design/tokens.css)
|
||
- Navegação: [design/navigation-ia.md](./design/navigation-ia.md)
|
||
|
||
---
|
||
|
||
## Phase 4: Runbooks + aprovações (VM122)
|
||
|
||
| Código | Nível | Executor |
|
||
|--------|-------|----------|
|
||
| `infra_recheck` | R0 | HTTP VM112 `/api/onboarding/infrastructure/status/{domain}` |
|
||
| `traefik_cert_sync` | R1 | Script/API CT114 (Spec 011) |
|
||
| `admin_nginx_reload` | R1 | SSH VM112 ou API futura |
|
||
| `zmproxy_admin_provision` | R2 | SSH VM112 — humano obrigatório |
|
||
| `domain_purge` | R3 | API VM122 → VM112 Spec 017 — dupla aprovação |
|
||
| `wazuh_acknowledge` | R1 | Nota + tag chamado (VM104 ack futuro) |
|
||
| `escalate_kimi_human` | R0 | Link OB-* ticket → chamado |
|
||
|
||
Fluxo R1:
|
||
|
||
```text
|
||
Técnico clica Executar → runbook_execucoes status=pendente
|
||
→ Agentic (opcional) gera proposta → aprovacoes fila
|
||
→ Humano Aprovar → worker executa → timeline + status ok|falhou
|
||
```
|
||
|
||
---
|
||
|
||
## Phase 5: Assist Opção A (VM112 + Console)
|
||
|
||
**VM112 — novo endpoint:**
|
||
|
||
```
|
||
GET /api/onboarding/session/{session_id}/ops-status
|
||
Header: X-Ops-Secret: <OPS_STATUS_SECRET>
|
||
LAN only: 10.10.10.0/24
|
||
```
|
||
|
||
Resposta:
|
||
|
||
```json
|
||
{
|
||
"session_id": "abc…",
|
||
"domain": "myvexx.com",
|
||
"wizard_step": "dns_cloudflare",
|
||
"wizard_step_label": "DNS Cloudflare",
|
||
"time_on_step_sec": 142,
|
||
"last_error": null,
|
||
"planned_email": "admin@myvexx.com"
|
||
}
|
||
```
|
||
|
||
**VM122:** poll 30s no detalhe chamado ou cache Redis `ops:session:{id}`.
|
||
|
||
**Console:** painel lateral `AssistPanel` — sem iframe wizard, sem senhas.
|
||
|
||
---
|
||
|
||
## Phase 6: Agentic + WebSocket (pós-MVP)
|
||
|
||
- Agente A6 analisa chamado → propõe runbook R0/R1
|
||
- Políticas em `agentic/policies.yaml` — confiança mínima, allowlist runbooks
|
||
- WebSocket `live` push: novo evento + passo wizard (substitui poll)
|
||
|
||
---
|
||
|
||
## Implementation Phases (time estimate)
|
||
|
||
| Fase | Entrega | VM | ~Semanas |
|
||
|------|---------|-----|----------|
|
||
| **F0** | Preflight VM123 + Traefik DNS | 123, 114 | 0.5 |
|
||
| **F1** | Schema + aggregator | 122 | 1–1.5 |
|
||
| **F2** | API chamados | 122 | 1.5–2 |
|
||
| **F3** | Console Docker MVP (3 views) | **123** | 3–4 |
|
||
| **F4** | Runbooks R0–R2 + aprovações | 122, 123 | 3–4 |
|
||
| **F5** | Assist ops-status | 112, 122, 123 | 2–3 |
|
||
| **F6** | Agentic + WS | 122, 123 | 3–4 |
|
||
|
||
**MVP (F0–F4):** ~10–12 semanas
|
||
**Completo (F0–F6):** ~16–20 semanas
|
||
|
||
---
|
||
|
||
## Sequencing with other specs
|
||
|
||
| Spec | Relação |
|
||
|------|---------|
|
||
| **001/002** | Eventos ingress → aggregator alimenta chamados |
|
||
| **004** | Funil widget no Overview; timeline enriquece chamado |
|
||
| **009** | Tenants view + scorecard drill-down |
|
||
| **010** | Runbooks admin + `admin.validation.failed` → chamado |
|
||
| **017/018** | Purge R3 + tile Serviços linka chamado |
|
||
|
||
**Ordem recomendada:** F1+F2 (API) em paralelo com F0+F3 scaffold UI Docker; F4 após API estável; F5 quando chamado único validado.
|
||
|
||
---
|
||
|
||
## Risk & Mitigation
|
||
|
||
| Risco | Mitigação |
|
||
|-------|-----------|
|
||
| Porta 8100 ocupada na VM123 | `preflight-vm123.sh`; variável `CONSOLE_HOST_PORT` no `.env` |
|
||
| Docker compose conflita rede | Rede bridge dedicada `ligbox-console`; não usar `network_mode: host` |
|
||
| CORS Console ↔ API | Allowlist `console.ligbox.com.br` no FastAPI VM122 |
|
||
| Agregação errada (chamados duplicados) | Regra session_id first; ferramenta merge manual ops |
|
||
| Runbook destrutivo sem R3 | Middleware bloqueia `domain_purge` sem dupla aprovação |
|
||
| VM123 serviços desconhecidos | Inventário obrigatório F0 antes deploy |
|
||
|
||
---
|
||
|
||
## Deploy checklist (cutover)
|
||
|
||
- [ ] `preflight-vm123.sh` OK
|
||
- [ ] `docker compose ps` — só `ligbox-ops-console-ui` healthy
|
||
- [ ] `curl -I https://console.ligbox.com.br/health` → 200
|
||
- [ ] Login JWT Spec 003 funcional
|
||
- [ ] Chamado teste `CH-*` com evento Wazuh + onboard
|
||
- [ ] `desk.ligbox.com.br` → 302 `console.ligbox.com.br` (após 90d aviso)
|
||
- [ ] Rollback: `docker compose down` no dir `/opt/ligbox-ops-console` apenas
|
||
|
||
---
|
||
|
||
## Referências
|
||
|
||
- [spec.md](./spec.md)
|
||
- [tasks.md](./tasks.md)
|
||
- [deploy/docker-compose.yml](./deploy/docker-compose.yml)
|
||
- [contracts/chamados-api.md](./contracts/chamados-api.md)
|