feat: enhance security features and backend schema

- Added new cron jobs for automatic IP block expiration and threat intelligence synchronization to improve security management.
- Expanded the backend schema to include various types of cyber attack classifications, security event statuses, and incident action types for better incident tracking and response.
- Introduced new tables for network sensors, IP reputation, port rules, threat intelligence feeds, and security events to enhance the overall security infrastructure.
- Updated API definitions to incorporate new security-related modules, ensuring comprehensive access to security functionalities.
This commit is contained in:
2025-11-15 07:25:01 -03:00
parent 118051ad56
commit ea01e2401a
11 changed files with 2781 additions and 472 deletions

View File

@@ -0,0 +1,714 @@
<script lang="ts">
import { browser } from '$app/environment';
import { useQuery, useConvexClient } from 'convex-svelte';
import type { FunctionReturnType } from 'convex/server';
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
import { api } from '@sgse-app/backend/convex/_generated/api';
import type { AtaqueCiberneticoTipo, SeveridadeSeguranca } from '@sgse-app/backend/convex/schema';
import { authStore } from '$lib/stores/auth.svelte';
type SerieCamada = FunctionReturnType<typeof api.security.obterVisaoCamadas>['series'][number];
type VisaoCamadas = FunctionReturnType<typeof api.security.obterVisaoCamadas>;
type EventosSeguranca = FunctionReturnType<typeof api.security.listarEventosSeguranca>;
type ReputacoesData = FunctionReturnType<typeof api.security.listarReputacoes>;
type RegrasPortaData = FunctionReturnType<typeof api.security.listarRegrasPorta>;
const client = useConvexClient();
const visaoCamadas = useQuery(api.security.obterVisaoCamadas, { periodoHoras: 6, buckets: 28 });
const eventos = useQuery(api.security.listarEventosSeguranca, { limit: 120 });
const reputacoes = useQuery(api.security.listarReputacoes, { limit: 60, lista: 'blacklist' });
const regrasPorta = useQuery(api.security.listarRegrasPorta, {});
const severidadesDisponiveis: SeveridadeSeguranca[] = ['informativo', 'baixo', 'moderado', 'alto', 'critico'];
const severityLabels: Record<SeveridadeSeguranca, string> = {
informativo: 'Informativo',
baixo: 'Baixo',
moderado: 'Moderado',
alto: 'Alto',
critico: 'Crítico'
};
const severityStyles: Record<SeveridadeSeguranca, string> = {
informativo: 'badge badge-ghost',
baixo: 'badge badge-success',
moderado: 'badge badge-info',
alto: 'badge badge-warning',
critico: 'badge badge-error'
};
const attackLabels: Record<AtaqueCiberneticoTipo, string> = {
phishing: 'Phishing',
malware: 'Malware',
ransomware: 'Ransomware',
brute_force: 'Brute Force',
credential_stuffing: 'Credential Stuffing',
sql_injection: 'SQL Injection',
xss: 'XSS',
man_in_the_middle: 'MITM',
ddos: 'DDoS',
engenharia_social: 'Engenharia Social',
cve_exploit: 'Exploração de CVE',
apt: 'APT',
zero_day: 'Zero-Day',
supply_chain: 'Supply Chain',
fileless_malware: 'Fileless Malware',
polymorphic_malware: 'Polymorphic',
ransomware_lateral: 'Ransomware Lateral',
deepfake_phishing: 'Deepfake Phishing',
adversarial_ai: 'Ataque IA',
side_channel: 'Side-Channel',
firmware_bootloader: 'Firmware/Bootloader',
bec: 'BEC',
botnet: 'Botnet',
ot_ics: 'OT/ICS',
quantum_attack: 'Quantum'
};
const statusStyles: Record<string, string> = {
detectado: 'badge badge-outline border-info/60 text-info',
investigando: 'badge badge-secondary',
contido: 'badge badge-success',
'falso_positivo': 'badge badge-ghost',
escalado: 'badge badge-error animate-pulse',
resolvido: 'badge badge-neutral'
};
let filtroSeveridades = $state<SeveridadeSeguranca[]>(['critico', 'alto']);
let filtroTipos = $state<AtaqueCiberneticoTipo[]>([]);
let alertaSonoroAtivo = $state(true);
let alertaVisualAtivo = $state(true);
let ipManual = $state('');
let comentarioManual = $state('');
let porta = $state(443);
let protocolo = $state<'tcp' | 'udp' | 'icmp' | 'quic' | 'any'>('tcp');
let acao = $state<'permitir' | 'bloquear' | 'monitorar' | 'rate_limit'>('monitorar');
let temporario = $state(true);
let duracao = $state(900);
let severidadeMin = $state<SeveridadeSeguranca>('moderado');
let relatorioInicio = $state(new Date(Date.now() - 60 * 60 * 1000).toISOString().slice(0, 16));
let relatorioFim = $state(new Date().toISOString().slice(0, 16));
let incluirMetricas = $state(true);
let incluirAcoes = $state(true);
let feedback = $state<{ tipo: 'success' | 'error'; mensagem: string } | null>(null);
let ultimaReferenciaCritica: Id<'securityEvents'> | null = null;
let audioCtx: AudioContext | null = null;
const series = $derived.by(() => visaoCamadas?.data?.series ?? []);
const totais = $derived.by(() => visaoCamadas?.data?.totais ?? null);
const eventosFiltrados = $derived.by(() => {
const lista = eventos?.data ?? [];
return lista
.filter((evento) => {
if (filtroSeveridades.length && !filtroSeveridades.includes(evento.severidade)) {
return false;
}
if (filtroTipos.length && !filtroTipos.includes(evento.tipoAtaque)) {
return false;
}
return true;
})
.slice(0, 50);
});
const ipCriticos = $derived.by(() => (reputacoes?.data ?? []).slice(0, 10));
const regras = $derived.by(() => regrasPorta?.data ?? []);
const eventoCriticoAtual = $derived.by(() => (eventos?.data ?? []).find((evento) => evento.severidade === 'critico') ?? null);
function maxSeriesValue(dataset: Array<Array<number>>): number {
let max = 1;
for (const serie of dataset) {
for (const valor of serie) {
if (valor > max) max = valor;
}
}
return max;
}
function construirLayer(valores: number[], largura: number, altura: number, maxValor: number): string {
if (!valores.length) {
return '';
}
const passo = valores.length > 1 ? largura / (valores.length - 1) : largura;
const pontos = valores.map((valor, indice) => {
const x = indice * passo;
const proporcao = maxValor === 0 ? 0 : valor / maxValor;
const y = altura - proporcao * altura;
return `${x},${Number.isFinite(y) ? y : altura}`;
});
return `M0,${altura} ${pontos.map((ponto) => `L${ponto}`).join(' ')} L${largura},${altura} Z`;
}
const chartPaths = $derived.by(() => {
if (!series.length) {
return { ddos: '', sql: '', avancados: '' };
}
const largura = 880;
const altura = 220;
const ddos = series.map((bucket) => bucket.ddos);
const sql = series.map((bucket) => bucket.sqlInjection);
const avancados = series.map((bucket) => bucket.avancados);
const maxValor = maxSeriesValue([ddos, sql, avancados]);
return {
ddos: construirLayer(ddos, largura, altura, maxValor),
sql: construirLayer(sql, largura, altura, maxValor),
avancados: construirLayer(avancados, largura, altura, maxValor)
};
});
function toggleFiltroSeveridade(item: SeveridadeSeguranca) {
if (filtroSeveridades.includes(item)) {
filtroSeveridades = filtroSeveridades.filter((valor) => valor !== item);
} else {
filtroSeveridades = [...filtroSeveridades, item];
}
}
function toggleFiltroTipo(item: AtaqueCiberneticoTipo) {
if (filtroTipos.includes(item)) {
filtroTipos = filtroTipos.filter((valor) => valor !== item);
} else {
filtroTipos = [...filtroTipos, item];
}
}
$effect(() => {
if (!eventoCriticoAtual || !alertaSonoroAtivo) {
return;
}
if (ultimaReferenciaCritica === eventoCriticoAtual._id) {
return;
}
ultimaReferenciaCritica = eventoCriticoAtual._id;
if (!browser) {
return;
}
if (!audioCtx) {
audioCtx = new AudioContext();
}
const oscillator = audioCtx.createOscillator();
const gain = audioCtx.createGain();
oscillator.type = 'triangle';
oscillator.frequency.value = 880;
gain.gain.value = 0.08;
oscillator.connect(gain).connect(audioCtx.destination);
oscillator.start();
setTimeout(() => {
oscillator.stop();
}, 300);
});
function mensagemErro(erro: unknown): string {
if (erro instanceof Error) return erro.message;
return 'Operação não concluída.';
}
function obterUsuarioId(): Id<'usuarios'> {
if (!authStore.usuario?._id) {
throw new Error('Usuário não autenticado.');
}
return authStore.usuario._id as Id<'usuarios'>;
}
async function aplicarMedidaIp(acaoSelecionada: 'forcar_blacklist' | 'remover_blacklist', ipAlvo: string, eventoId?: Id<'securityEvents'>) {
if (!ipAlvo) {
feedback = { tipo: 'error', mensagem: 'Informe o IP alvo.' };
return;
}
try {
await client.mutation(api.security.atualizarReputacaoIndicador, {
usuarioId: obterUsuarioId(),
indicador: ipAlvo,
categoria: 'ip',
acao: acaoSelecionada,
delta: acaoSelecionada === 'forcar_blacklist' ? -30 : 10,
comentario: comentarioManual || `Ação manual em ${new Date().toLocaleString()}`,
duracaoSegundos: acaoSelecionada === 'forcar_blacklist' ? 3600 : undefined
});
if (eventoId) {
await client.mutation(api.security.registrarAcaoIncidente, {
eventoId,
tipo: acaoSelecionada === 'forcar_blacklist' ? 'block_ip' : 'unblock_ip',
origem: 'manual',
executadoPor: obterUsuarioId(),
detalhes: comentarioManual || 'Ação executada via Wizcard',
resultado: 'registrado',
relacionadoA: undefined
});
}
feedback = {
tipo: 'success',
mensagem:
acaoSelecionada === 'forcar_blacklist'
? `IP ${ipAlvo} bloqueado e registrado na blacklist.`
: `IP ${ipAlvo} liberado e removido da blacklist.`
};
ipManual = '';
comentarioManual = '';
} catch (erro: unknown) {
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
}
}
async function salvarRegraPorta(e: SubmitEvent) {
e.preventDefault();
try {
await client.mutation(api.security.configurarRegraPorta, {
usuarioId: obterUsuarioId(),
porta,
protocolo,
acao,
temporario,
duracaoSegundos: temporario ? duracao : undefined,
severidadeMin,
notas: `Regra criada via Wizcard em ${new Date().toLocaleString()}`,
tags: ['cibersecurity', acao],
listaReferencia: undefined
});
feedback = { tipo: 'success', mensagem: `Regra para porta ${porta}/${protocolo.toUpperCase()} registrada.` };
} catch (erro: unknown) {
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
}
}
function parseDatetime(valor: string): number {
if (!valor) return Date.now();
const data = new Date(valor);
return Number.isNaN(data.getTime()) ? Date.now() : data.getTime();
}
async function gerarRelatorioAvancado(e: SubmitEvent) {
e.preventDefault();
try {
await client.mutation(api.security.solicitarRelatorioSeguranca, {
solicitanteId: obterUsuarioId(),
filtros: {
dataInicio: parseDatetime(relatorioInicio),
dataFim: parseDatetime(relatorioFim),
severidades: filtroSeveridades.length ? filtroSeveridades : undefined,
tiposAtaque: filtroTipos.length ? filtroTipos : undefined,
incluirIndicadores: true,
incluirMetricas,
incluirAcoes
}
});
feedback = {
tipo: 'success',
mensagem: 'Relatório refinado em processamento. Você será notificado em instantes.'
};
} catch (erro: unknown) {
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
}
}
function classeEventoCritico(severidade: SeveridadeSeguranca): string {
if (!alertaVisualAtivo) return '';
return severidade === 'critico' ? 'ring-2 ring-error animate-pulse' : '';
}
function formatarData(timestamp: number): string {
const data = new Date(timestamp);
return data.toLocaleString('pt-BR', {
hour12: false
});
}
</script>
<div class="space-y-6">
{#if feedback}
<div class="alert shadow-lg {feedback.tipo === 'success' ? 'alert-success' : 'alert-error'}">
<div>
<span>{feedback.mensagem}</span>
</div>
<button type="button" class="btn btn-sm btn-ghost" onclick={() => (feedback = null)}>Fechar</button>
</div>
{/if}
<section class="grid gap-4 lg:grid-cols-4 md:grid-cols-2">
<div class="stat bg-base-100 rounded-2xl border border-primary/20 shadow-xl">
<div class="stat-title text-primary font-semibold">Eventos monitorados</div>
<div class="stat-value text-primary text-4xl">{totais?.eventos ?? 0}</div>
<div class="stat-desc">Últimas 6h</div>
</div>
<div class="stat bg-base-100 rounded-2xl border border-error/20 shadow-xl">
<div class="stat-title text-error font-semibold">Críticos</div>
<div class="stat-value text-error text-4xl">{totais?.criticos ?? 0}</div>
<div class="stat-desc">Escalonados imediatamente</div>
</div>
<div class="stat bg-base-100 rounded-2xl border border-warning/20 shadow-xl">
<div class="stat-title text-warning font-semibold">Bloqueios ativos</div>
<div class="stat-value text-warning text-4xl">{totais?.bloqueiosAtivos ?? 0}</div>
<div class="stat-desc">IPs e domínios isolados</div>
</div>
<div class="stat bg-base-100 rounded-2xl border border-success/20 shadow-xl">
<div class="stat-title text-success font-semibold">Sensores ativos</div>
<div class="stat-value text-success text-4xl">{totais?.sensoresAtivos ?? 0}</div>
<div class="stat-desc">Edge, OT e honeypots</div>
</div>
</section>
<section class="grid gap-6 lg:grid-cols-3">
<div class="rounded-3xl border border-primary/20 bg-base-100/80 p-6 shadow-2xl lg:col-span-2">
<div class="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 class="text-2xl font-bold text-primary">Layerchart Threat Matrix</h2>
<p class="text-base-content/70 text-sm">
Correlação temporal entre DDoS, SQLi, ataques avançados e bloqueios automáticos.
</p>
</div>
<div class="flex gap-2">
<label class="label cursor-pointer gap-2">
<span class="label-text">Alerta Sonoro</span>
<input type="checkbox" class="toggle toggle-primary" bind:checked={alertaSonoroAtivo} />
</label>
<label class="label cursor-pointer gap-2">
<span class="label-text">Flash Visual</span>
<input type="checkbox" class="toggle toggle-secondary" bind:checked={alertaVisualAtivo} />
</label>
</div>
</div>
<div class="mt-4 overflow-x-auto rounded-2xl bg-base-200/40 p-4">
<svg viewBox="0 0 880 220" class="w-full">
<defs>
<linearGradient id="grad-ddos" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="rgba(255, 76, 76, 0.8)" />
<stop offset="100%" stop-color="rgba(255, 76, 76, 0.1)" />
</linearGradient>
<linearGradient id="grad-sql" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="rgba(59, 130, 246, 0.8)" />
<stop offset="100%" stop-color="rgba(59, 130, 246, 0.1)" />
</linearGradient>
<linearGradient id="grad-adv" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="rgba(236, 72, 153, 0.8)" />
<stop offset="100%" stop-color="rgba(236, 72, 153, 0.1)" />
</linearGradient>
</defs>
<path d={chartPaths.avancados} fill="url(#grad-adv)" stroke="rgba(236, 72, 153, 0.6)" stroke-width="2" />
<path d={chartPaths.sql} fill="url(#grad-sql)" stroke="rgba(59, 130, 246, 0.6)" stroke-width="2" />
<path d={chartPaths.ddos} fill="url(#grad-ddos)" stroke="rgba(248, 113, 113, 0.8)" stroke-width="2" />
</svg>
<div class="mt-4 flex flex-wrap gap-3 text-sm font-semibold">
<span class="badge badge-error gap-2">
<span class="h-2 w-2 rounded-full bg-error"></span>DDoS
</span>
<span class="badge badge-info gap-2">
<span class="h-2 w-2 rounded-full bg-info"></span>SQL Injection
</span>
<span class="badge badge-secondary gap-2">
<span class="h-2 w-2 rounded-full bg-secondary"></span>ATA Advanced
</span>
</div>
</div>
<div class="mt-6 flex flex-wrap gap-2">
{#each severidadesDisponiveis as severidade}
<button
type="button"
class={`btn btn-xs ${filtroSeveridades.includes(severidade) ? 'btn-primary' : 'btn-outline btn-primary'}`}
onclick={() => toggleFiltroSeveridade(severidade)}
>
{severityLabels[severidade]}
</button>
{/each}
</div>
</div>
<div class="rounded-3xl border border-secondary/20 bg-base-100/80 p-6 shadow-2xl">
<h3 class="text-secondary text-xl font-bold">Ações rápidas</h3>
<form
class="mt-4 space-y-3"
onsubmit={(event) => {
event.preventDefault();
aplicarMedidaIp('forcar_blacklist', ipManual);
}}
>
<label class="form-control w-full">
<div class="label">
<span class="label-text">IP / Host</span>
</div>
<input
type="text"
class="input input-bordered w-full"
placeholder="Ex: 177.24.10.90"
bind:value={ipManual}
/>
</label>
<label class="form-control w-full">
<div class="label">
<span class="label-text">Comentário técnico</span>
</div>
<textarea
class="textarea textarea-bordered w-full"
rows="2"
placeholder="Ex: Bloqueio após heurística MITM no JA3."
bind:value={comentarioManual}
></textarea>
</label>
<div class="flex gap-2">
<button type="submit" class="btn btn-error flex-1">Bloquear IP</button>
<button
type="button"
class="btn btn-success flex-1"
onclick={() => aplicarMedidaIp('remover_blacklist', ipManual)}
>
Liberar
</button>
</div>
</form>
<div class="divider my-6">Regras de Porta</div>
<form class="space-y-3" onsubmit={salvarRegraPorta}>
<div class="grid grid-cols-2 gap-2">
<label class="form-control">
<div class="label">
<span class="label-text">Porta</span>
</div>
<input type="number" class="input input-bordered" min="1" max="65535" bind:value={porta} />
</label>
<label class="form-control">
<div class="label">
<span class="label-text">Protocolo</span>
</div>
<select class="select select-bordered" bind:value={protocolo}>
<option value="tcp">TCP</option>
<option value="udp">UDP</option>
<option value="icmp">ICMP</option>
<option value="quic">QUIC</option>
<option value="any">ANY</option>
</select>
</label>
</div>
<label class="form-control">
<div class="label">
<span class="label-text">Ação</span>
</div>
<select class="select select-bordered" bind:value={acao}>
<option value="permitir">Permitir</option>
<option value="bloquear">Bloquear</option>
<option value="monitorar">Monitorar</option>
<option value="rate_limit">Rate Limit</option>
</select>
</label>
<div class="flex items-center gap-2">
<input type="checkbox" class="toggle toggle-info" bind:checked={temporario} />
<span class="label-text">Bloqueio temporário?</span>
{#if temporario}
<input
type="number"
class="input input-bordered ml-auto w-32"
min="60"
step="60"
placeholder="Segundos"
bind:value={duracao}
/>
{/if}
</div>
<label class="form-control">
<div class="label">
<span class="label-text">Severidade mínima</span>
</div>
<select class="select select-bordered" bind:value={severidadeMin}>
{#each severidadesDisponiveis as severidade}
<option value={severidade}>{severityLabels[severidade]}</option>
{/each}
</select>
</label>
<button type="submit" class="btn btn-secondary w-full">Salvar Regra</button>
</form>
</div>
</section>
<section class="grid gap-6 lg:grid-cols-3">
<div class="rounded-3xl border border-base-300 bg-base-100 p-6 shadow-2xl lg:col-span-2">
<div class="flex flex-wrap items-center justify-between gap-3">
<h3 class="text-2xl font-semibold text-base-content">Feed de eventos e ameaças</h3>
<div class="flex flex-wrap gap-2">
{#each Object.entries(attackLabels).slice(0, 8) as [tipo, label]}
<button
type="button"
class={`btn btn-xs ${filtroTipos.includes(tipo as AtaqueCiberneticoTipo) ? 'btn-accent' : 'btn-outline btn-accent'}`}
onclick={() => toggleFiltroTipo(tipo as AtaqueCiberneticoTipo)}
>
{label}
</button>
{/each}
</div>
</div>
<div class="mt-4 space-y-4">
{#if eventosFiltrados.length === 0}
<p class="text-base-content/60 text-sm">Nenhum evento correspondente aos filtros.</p>
{:else}
{#each eventosFiltrados as evento}
<div class={`card border border-base-200 bg-base-100 shadow-lg ${classeEventoCritico(evento.severidade)}`}>
<div class="card-body gap-3">
<div class="flex flex-wrap items-center justify-between gap-3">
<div class="flex flex-wrap items-center gap-2">
<span class={severityStyles[evento.severidade]}>{severityLabels[evento.severidade]}</span>
<span class="badge badge-outline">{attackLabels[evento.tipoAtaque]}</span>
<span class={statusStyles[evento.status] ?? 'badge badge-ghost'}>{evento.status}</span>
</div>
<span class="text-xs text-base-content/60">{formatarData(evento.timestamp)}</span>
</div>
<p class="text-sm text-base-content/80">{evento.descricao}</p>
<div class="grid gap-2 text-xs md:grid-cols-2">
<div class="space-y-1">
<div class="flex items-center gap-2">
<span class="font-semibold">Origem:</span>
<span>{evento.origemIp ?? 'n/d'}</span>
</div>
<div class="flex items-center gap-2">
<span class="font-semibold">Destino:</span>
<span>{evento.destinoIp ?? 'n/d'}:{evento.destinoPorta ?? '--'}</span>
</div>
</div>
<div class="space-y-1">
<div class="flex items-center gap-2">
<span class="font-semibold">Protocolo:</span>
<span>{evento.protocolo ?? 'n/d'}</span>
</div>
<div class="flex items-center gap-2">
<span class="font-semibold">Tags:</span>
<span>{evento.tags?.join(', ') ?? '—'}</span>
</div>
</div>
</div>
<div class="flex flex-wrap gap-2">
<button
type="button"
class="btn btn-error btn-sm"
onclick={() => aplicarMedidaIp('forcar_blacklist', evento.origemIp ?? '', evento._id)}
>
Bloquear origem
</button>
<button
type="button"
class="btn btn-outline btn-sm"
onclick={async () => {
try {
await client.mutation(api.security.solicitarRelatorioSeguranca, {
solicitanteId: obterUsuarioId(),
filtros: {
dataInicio: evento.timestamp - 15 * 60 * 1000,
dataFim: evento.timestamp + 15 * 60 * 1000,
severidades: [evento.severidade],
tiposAtaque: [evento.tipoAtaque],
incluirIndicadores: true,
incluirMetricas: true,
incluirAcoes: true
}
});
feedback = {
tipo: 'success',
mensagem: 'Mini relatório solicitado para o evento selecionado.'
};
} catch (erro: unknown) {
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
}
}}
>
Relatório 30min
</button>
</div>
</div>
</div>
{/each}
{/if}
</div>
</div>
<div class="rounded-3xl border border-accent/20 bg-base-100 p-6 shadow-2xl space-y-6">
<div>
<h4 class="text-accent text-lg font-bold">Lista Negra Inteligente</h4>
<ul class="mt-4 space-y-3 text-sm">
{#if ipCriticos.length === 0}
<li class="text-base-content/60">Nenhum IP crítico listado.</li>
{:else}
{#each ipCriticos as registro}
<li class="flex items-center justify-between gap-2 rounded-xl bg-base-200/40 p-3">
<div>
<p class="font-semibold">{registro.indicador}</p>
<p class="text-xs text-base-content/60">
Score: {registro.reputacao} • Ocorrências: {registro.ocorrencias}
</p>
</div>
<button
type="button"
class="btn btn-xs btn-outline"
onclick={() => aplicarMedidaIp('remover_blacklist', registro.indicador)}
>
Liberar
</button>
</li>
{/each}
{/if}
</ul>
</div>
<div>
<h4 class="text-info text-lg font-bold">Regras de Portas Monitoradas</h4>
<div class="mt-4 space-y-2 text-xs">
{#if regras.length === 0}
<p class="text-base-content/60">Nenhuma regra cadastrada.</p>
{:else}
{#each regras as regra}
<div class="rounded-xl border border-base-200 p-3">
<div class="flex items-center justify-between">
<span class="font-semibold">
{regra.porta}/{regra.protocolo.toUpperCase()}
</span>
<span class="badge badge-outline">{regra.acao}</span>
</div>
<p class="text-base-content/70 mt-1">
Severidade mínima: {severityLabels[regra.severidadeMin]}
</p>
{#if regra.expiraEm}
<p class="text-base-content/50">
Expira em: {new Date(regra.expiraEm).toLocaleString('pt-BR', { hour12: false })}
</p>
{/if}
</div>
{/each}
{/if}
</div>
</div>
<div>
<h4 class="text-primary text-lg font-bold">Relatórios refinados</h4>
<form class="mt-3 space-y-2" onsubmit={gerarRelatorioAvancado}>
<label class="form-control">
<div class="label">
<span class="label-text text-xs">Início</span>
</div>
<input type="datetime-local" class="input input-bordered input-sm" bind:value={relatorioInicio} />
</label>
<label class="form-control">
<div class="label">
<span class="label-text text-xs">Fim</span>
</div>
<input type="datetime-local" class="input input-bordered input-sm" bind:value={relatorioFim} />
</label>
<label class="flex items-center gap-2 text-xs">
<input type="checkbox" class="checkbox checkbox-xs" bind:checked={incluirMetricas} />
<span>Incluir métricas e payload hash</span>
</label>
<label class="flex items-center gap-2 text-xs">
<input type="checkbox" class="checkbox checkbox-xs" bind:checked={incluirAcoes} />
<span>Incluir histórico de ações</span>
</label>
<button type="submit" class="btn btn-primary btn-sm w-full">Gerar Relatório</button>
</form>
</div>
</div>
</section>
</div>
<style>
.card::-webkit-scrollbar {
display: none;
}
</style>