feat: integrate rate limiting and enhance security features

- Added @convex-dev/rate-limiter dependency to manage request limits effectively.
- Implemented rate limiting configurations for IPs, users, and endpoints to prevent abuse and enhance security.
- Introduced new security analysis endpoint to detect potential attacks based on incoming requests.
- Updated backend schema to include rate limit configurations and various cyber attack types for improved incident tracking.
- Enhanced existing security functions to incorporate rate limiting checks, ensuring robust protection against brute force and other attacks.
This commit is contained in:
2025-11-16 01:20:57 -03:00
parent ea01e2401a
commit 88983ea297
19 changed files with 3102 additions and 109 deletions

View File

@@ -1,26 +1,29 @@
<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 configsRateLimit = useQuery(api.security.listarConfigsRateLimit, {
ativo: true,
limit: 50
});
const meuPerfil = useQuery(api.usuarios.obterPerfil, {});
const severidadesDisponiveis: SeveridadeSeguranca[] = ['informativo', 'baixo', 'moderado', 'alto', 'critico'];
const severidadesDisponiveis: SeveridadeSeguranca[] = [
'informativo',
'baixo',
'moderado',
'alto',
'critico'
];
const severityLabels: Record<SeveridadeSeguranca, string> = {
informativo: 'Informativo',
@@ -46,6 +49,10 @@
credential_stuffing: 'Credential Stuffing',
sql_injection: 'SQL Injection',
xss: 'XSS',
path_traversal: 'Path Traversal',
command_injection: 'Command Injection',
nosql_injection: 'NoSQL Injection',
xxe: 'XXE',
man_in_the_middle: 'MITM',
ddos: 'DDoS',
engenharia_social: 'Engenharia Social',
@@ -70,15 +77,18 @@
detectado: 'badge badge-outline border-info/60 text-info',
investigando: 'badge badge-secondary',
contido: 'badge badge-success',
'falso_positivo': 'badge badge-ghost',
falso_positivo: 'badge badge-ghost',
escalado: 'badge badge-error animate-pulse',
resolvido: 'badge badge-neutral'
};
let filtroSeveridades = $state<SeveridadeSeguranca[]>(['critico', 'alto']);
let filtroSeveridades = $state<SeveridadeSeguranca[]>([]); // Vazio = mostrar todos
let filtroTipos = $state<AtaqueCiberneticoTipo[]>([]);
let alertaSonoroAtivo = $state(true);
let alertaVisualAtivo = $state(true);
// Contagem de novos eventos detectados em tempo real (sem recarregar)
let novosEventos = $state(0);
let ultimoTotalEventos: number | null = null;
let ipManual = $state('');
let comentarioManual = $state('');
let porta = $state(443);
@@ -92,12 +102,28 @@
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 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(() => {
// Rate Limiting
let mostrarRateLimitConfig = $state(false);
let rateLimitNome = $state('');
let rateLimitTipo = $state<'ip' | 'usuario' | 'endpoint' | 'global'>('ip');
let rateLimitIdentificador = $state('');
let rateLimitLimite = $state(100);
let rateLimitJanelaSegundos = $state(60);
let rateLimitEstrategia = $state<'fixed_window' | 'sliding_window' | 'token_bucket'>(
'fixed_window'
);
let rateLimitAcaoExcedido = $state<'bloquear' | 'throttle' | 'alertar'>('bloquear');
let rateLimitBloqueioTemporarioSegundos = $state<number | undefined>(undefined);
let rateLimitPrioridade = $state(0);
let rateLimitNotas = $state('');
let rateLimitEditando = $state<Id<'rateLimitConfig'> | 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) => {
@@ -111,10 +137,34 @@ const eventosFiltrados = $derived.by(() => {
})
.slice(0, 50);
});
const ipCriticos = $derived.by(() => (reputacoes?.data ?? []).slice(0, 10));
const regras = $derived.by(() => regrasPorta?.data ?? []);
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);
const eventoCriticoAtual = $derived.by(
() => (eventos?.data ?? []).find((evento) => evento.severidade === 'critico') ?? null
);
// Efeito: observar chegada de novos eventos e acionar toast/contador
$effect(() => {
const atual = (eventos?.data ?? []).length;
if (ultimoTotalEventos === null) {
ultimoTotalEventos = atual;
return;
}
if (atual > ultimoTotalEventos) {
const delta = atual - ultimoTotalEventos;
novosEventos += delta;
feedback = {
tipo: 'success',
mensagem: `🔔 ${delta} novo(s) evento(s) de segurança detectado(s) em tempo real`
};
// Opcional: destacar visualmente
if (alertaVisualAtivo) {
// classe CSS já existente de alertas visuais; aqui mantemos apenas o toast
}
}
ultimoTotalEventos = atual;
});
function maxSeriesValue(dataset: Array<Array<number>>): number {
let max = 1;
@@ -126,7 +176,12 @@ const eventoCriticoAtual = $derived.by(() => (eventos?.data ?? []).find((evento)
return max;
}
function construirLayer(valores: number[], largura: number, altura: number, maxValor: number): string {
function construirLayer(
valores: number[],
largura: number,
altura: number,
maxValor: number
): string {
if (!valores.length) {
return '';
}
@@ -140,7 +195,7 @@ const eventoCriticoAtual = $derived.by(() => (eventos?.data ?? []).find((evento)
return `M0,${altura} ${pontos.map((ponto) => `L${ponto}`).join(' ')} L${largura},${altura} Z`;
}
const chartPaths = $derived.by(() => {
const chartPaths = $derived.by(() => {
if (!series.length) {
return { ddos: '', sql: '', avancados: '' };
}
@@ -205,13 +260,24 @@ const chartPaths = $derived.by(() => {
}
function obterUsuarioId(): Id<'usuarios'> {
if (!authStore.usuario?._id) {
throw new Error('Usuário não autenticado.');
// Prioridade: usar query do Convex (mais confiável) > authStore
// Se a query retornou dados, usar o ID do Convex
if (meuPerfil?.data?._id) {
return meuPerfil.data._id;
}
return authStore.usuario._id as Id<'usuarios'>;
// Se a query ainda está carregando (undefined) ou retornou null, tentar authStore
if (authStore.autenticado && authStore.usuario?._id) {
return authStore.usuario._id as Id<'usuarios'>;
}
// Se nenhum dos dois funcionou, lançar erro
throw new Error('Usuário não autenticado. Por favor, faça login no sistema.');
}
async function aplicarMedidaIp(acaoSelecionada: 'forcar_blacklist' | 'remover_blacklist', ipAlvo: string, eventoId?: Id<'securityEvents'>) {
async function aplicarMedidaIp(
acaoSelecionada: 'forcar_blacklist' | 'remover_blacklist',
ipAlvo: string,
eventoId?: Id<'securityEvents'>
) {
if (!ipAlvo) {
feedback = { tipo: 'error', mensagem: 'Informe o IP alvo.' };
return;
@@ -268,7 +334,10 @@ const chartPaths = $derived.by(() => {
tags: ['cibersecurity', acao],
listaReferencia: undefined
});
feedback = { tipo: 'success', mensagem: `Regra para porta ${porta}/${protocolo.toUpperCase()} registrada.` };
feedback = {
tipo: 'success',
mensagem: `Regra para porta ${porta}/${protocolo.toUpperCase()} registrada.`
};
} catch (erro: unknown) {
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
}
@@ -304,6 +373,111 @@ const chartPaths = $derived.by(() => {
}
}
function resetarFormRateLimit() {
rateLimitNome = '';
rateLimitTipo = 'ip';
rateLimitIdentificador = '';
rateLimitLimite = 100;
rateLimitJanelaSegundos = 60;
rateLimitEstrategia = 'fixed_window';
rateLimitAcaoExcedido = 'bloquear';
rateLimitBloqueioTemporarioSegundos = undefined;
rateLimitPrioridade = 0;
rateLimitNotas = '';
rateLimitEditando = null;
}
function editarConfigRateLimit(configId: Id<'rateLimitConfig'>) {
const config = configsRateLimit?.data?.find((c) => c._id === configId);
if (!config) return;
rateLimitNome = config.nome;
rateLimitTipo = config.tipo;
rateLimitIdentificador = config.identificador ?? '';
rateLimitLimite = config.limite;
rateLimitJanelaSegundos = config.janelaSegundos;
rateLimitEstrategia = config.estrategia;
rateLimitAcaoExcedido = config.acaoExcedido;
rateLimitBloqueioTemporarioSegundos = config.bloqueioTemporarioSegundos;
rateLimitPrioridade = config.prioridade;
rateLimitNotas = config.notas ?? '';
rateLimitEditando = configId;
mostrarRateLimitConfig = true;
}
async function salvarConfigRateLimit(e: SubmitEvent) {
e.preventDefault();
try {
if (rateLimitEditando) {
await client.mutation(api.security.atualizarConfigRateLimit, {
configId: rateLimitEditando,
usuarioId: obterUsuarioId(),
nome: rateLimitNome,
limite: rateLimitLimite,
janelaSegundos: rateLimitJanelaSegundos,
estrategia: rateLimitEstrategia,
acaoExcedido: rateLimitAcaoExcedido,
bloqueioTemporarioSegundos: rateLimitBloqueioTemporarioSegundos,
prioridade: rateLimitPrioridade,
notas: rateLimitNotas || undefined
});
feedback = {
tipo: 'success',
mensagem: 'Configuração de rate limit atualizada com sucesso.'
};
} else {
await client.mutation(api.security.criarConfigRateLimit, {
usuarioId: obterUsuarioId(),
nome: rateLimitNome,
tipo: rateLimitTipo,
identificador:
rateLimitTipo === 'global' ? undefined : rateLimitIdentificador || undefined,
limite: rateLimitLimite,
janelaSegundos: rateLimitJanelaSegundos,
estrategia: rateLimitEstrategia,
acaoExcedido: rateLimitAcaoExcedido,
bloqueioTemporarioSegundos: rateLimitBloqueioTemporarioSegundos,
prioridade: rateLimitPrioridade,
notas: rateLimitNotas || undefined
});
feedback = { tipo: 'success', mensagem: 'Configuração de rate limit criada com sucesso.' };
}
resetarFormRateLimit();
mostrarRateLimitConfig = false;
} catch (erro: unknown) {
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
}
}
async function deletarConfigRateLimit(configId: Id<'rateLimitConfig'>) {
if (!confirm('Tem certeza que deseja deletar esta configuração de rate limit?')) return;
try {
await client.mutation(api.security.deletarConfigRateLimit, {
configId,
usuarioId: obterUsuarioId()
});
feedback = { tipo: 'success', mensagem: 'Configuração de rate limit deletada com sucesso.' };
} catch (erro: unknown) {
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
}
}
async function toggleAtivoRateLimit(configId: Id<'rateLimitConfig'>, ativo: boolean) {
try {
await client.mutation(api.security.atualizarConfigRateLimit, {
configId,
usuarioId: obterUsuarioId(),
ativo: !ativo
});
feedback = {
tipo: 'success',
mensagem: `Configuração ${!ativo ? 'ativada' : 'desativada'} com sucesso.`
};
} 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' : '';
@@ -323,27 +497,29 @@ const chartPaths = $derived.by(() => {
<div>
<span>{feedback.mensagem}</span>
</div>
<button type="button" class="btn btn-sm btn-ghost" onclick={() => (feedback = null)}>Fechar</button>
<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">
<section class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<div class="stat bg-base-100 border-primary/20 rounded-2xl border 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 bg-base-100 border-error/20 rounded-2xl border 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 bg-base-100 border-warning/20 rounded-2xl border 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 bg-base-100 border-success/20 rounded-2xl border 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>
@@ -351,10 +527,10 @@ const chartPaths = $derived.by(() => {
</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="border-primary/20 bg-base-100/80 rounded-3xl border 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>
<h2 class="text-primary text-2xl font-bold">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>
@@ -366,12 +542,16 @@ const chartPaths = $derived.by(() => {
</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} />
<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">
<div class="bg-base-200/40 mt-4 overflow-x-auto rounded-2xl p-4">
<svg viewBox="0 0 880 220" class="w-full">
<defs>
<linearGradient id="grad-ddos" x1="0%" y1="0%" x2="0%" y2="100%">
@@ -387,25 +567,40 @@ const chartPaths = $derived.by(() => {
<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" />
<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 class="bg-error h-2 w-2 rounded-full"></span>DDoS
</span>
<span class="badge badge-info gap-2">
<span class="h-2 w-2 rounded-full bg-info"></span>SQL Injection
<span class="bg-info h-2 w-2 rounded-full"></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 class="bg-secondary h-2 w-2 rounded-full"></span>ATA Advanced
</span>
</div>
</div>
<div class="mt-6 flex flex-wrap gap-2">
{#each severidadesDisponiveis as severidade}
{#each severidadesDisponiveis as severidade (severidade)}
<button
type="button"
class={`btn btn-xs ${filtroSeveridades.includes(severidade) ? 'btn-primary' : 'btn-outline btn-primary'}`}
@@ -417,7 +612,7 @@ const chartPaths = $derived.by(() => {
</div>
</div>
<div class="rounded-3xl border border-secondary/20 bg-base-100/80 p-6 shadow-2xl">
<div class="border-secondary/20 bg-base-100/80 rounded-3xl border p-6 shadow-2xl">
<h3 class="text-secondary text-xl font-bold">Ações rápidas</h3>
<form
class="mt-4 space-y-3"
@@ -467,7 +662,13 @@ const chartPaths = $derived.by(() => {
<div class="label">
<span class="label-text">Porta</span>
</div>
<input type="number" class="input input-bordered" min="1" max="65535" bind:value={porta} />
<input
type="number"
class="input input-bordered"
min="1"
max="65535"
bind:value={porta}
/>
</label>
<label class="form-control">
<div class="label">
@@ -512,7 +713,7 @@ const chartPaths = $derived.by(() => {
<span class="label-text">Severidade mínima</span>
</div>
<select class="select select-bordered" bind:value={severidadeMin}>
{#each severidadesDisponiveis as severidade}
{#each severidadesDisponiveis as severidade (severidade)}
<option value={severidade}>{severityLabels[severidade]}</option>
{/each}
</select>
@@ -523,11 +724,63 @@ const chartPaths = $derived.by(() => {
</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="border-base-300 bg-base-100 rounded-3xl border 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>
<h3 class="text-base-content text-2xl font-semibold">Feed de eventos e ameaças</h3>
<div class="flex flex-wrap gap-2">
{#each Object.entries(attackLabels).slice(0, 8) as [tipo, label]}
{#if novosEventos > 0}
<button
type="button"
class="btn btn-xs btn-primary"
title="Novos eventos detectados"
onclick={() => (novosEventos = 0)}
>
🔔 Novos: <span class="ml-1 font-bold">{novosEventos}</span>
</button>
{/if}
<button
type="button"
class="btn btn-xs btn-warning"
onclick={async () => {
try {
const resultado = await client.mutation(api.security.criarEventosTeste, { quantidade: 10 });
feedback = {
tipo: 'success',
mensagem: `✅ ${resultado.eventosCriados} eventos de teste criados com sucesso!`
};
} catch (error) {
feedback = {
tipo: 'error',
mensagem: `❌ Erro ao criar eventos de teste: ${error}`
};
}
}}
>
🧪 Criar Eventos Teste
</button>
<button
type="button"
class="btn btn-xs btn-outline"
onclick={async () => {
try {
const resultado = await client.mutation(api.security.limparEventosTeste, {});
feedback = {
tipo: 'success',
mensagem: `🧹 ${resultado.removidos} eventos de teste removidos`
};
novosEventos = 0;
ultimoTotalEventos = (eventos?.data ?? []).length - (resultado.removidos ?? 0);
} catch (error) {
feedback = {
tipo: 'error',
mensagem: `❌ Erro ao limpar eventos de teste: ${error}`
};
}
}}
>
🧹 Limpar Eventos Teste
</button>
{#each Object.entries(attackLabels).slice(0, 8) as [tipo, label] (tipo)}
<button
type="button"
class={`btn btn-xs ${filtroTipos.includes(tipo as AtaqueCiberneticoTipo) ? 'btn-accent' : 'btn-outline btn-accent'}`}
@@ -542,18 +795,24 @@ const chartPaths = $derived.by(() => {
{#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)}`}>
{#each eventosFiltrados as evento (evento._id)}
<div
class={`card border-base-200 bg-base-100 border 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={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>
<span class={statusStyles[evento.status] ?? 'badge badge-ghost'}
>{evento.status}</span
>
</div>
<span class="text-xs text-base-content/60">{formatarData(evento.timestamp)}</span>
<span class="text-base-content/60 text-xs">{formatarData(evento.timestamp)}</span>
</div>
<p class="text-sm text-base-content/80">{evento.descricao}</p>
<p class="text-base-content/80 text-sm">{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">
@@ -580,7 +839,8 @@ const chartPaths = $derived.by(() => {
<button
type="button"
class="btn btn-error btn-sm"
onclick={() => aplicarMedidaIp('forcar_blacklist', evento.origemIp ?? '', evento._id)}
onclick={() =>
aplicarMedidaIp('forcar_blacklist', evento.origemIp ?? '', evento._id)}
>
Bloquear origem
</button>
@@ -620,18 +880,18 @@ const chartPaths = $derived.by(() => {
</div>
</div>
<div class="rounded-3xl border border-accent/20 bg-base-100 p-6 shadow-2xl space-y-6">
<div class="border-accent/20 bg-base-100 space-y-6 rounded-3xl border p-6 shadow-2xl">
<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">
{#each ipCriticos as registro (registro.indicador)}
<li class="bg-base-200/40 flex items-center justify-between gap-2 rounded-xl p-3">
<div>
<p class="font-semibold">{registro.indicador}</p>
<p class="text-xs text-base-content/60">
<p class="text-base-content/60 text-xs">
Score: {registro.reputacao} Ocorrências: {registro.ocorrencias}
</p>
</div>
@@ -654,8 +914,8 @@ const chartPaths = $derived.by(() => {
{#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">
{#each regras as regra (regra._id)}
<div class="border-base-200 rounded-xl border p-3">
<div class="flex items-center justify-between">
<span class="font-semibold">
{regra.porta}/{regra.protocolo.toUpperCase()}
@@ -670,6 +930,48 @@ const chartPaths = $derived.by(() => {
Expira em: {new Date(regra.expiraEm).toLocaleString('pt-BR', { hour12: false })}
</p>
{/if}
<div class="mt-2 flex gap-2">
<button
type="button"
class="btn btn-xs btn-outline"
onclick={async () => {
try {
await client.mutation(api.security.configurarRegraPorta, {
usuarioId: obterUsuarioId(),
regraId: regra._id,
porta: regra.porta,
protocolo: regra.protocolo,
acao: regra.acao,
temporario: false,
severidadeMin: regra.severidadeMin
});
feedback = { tipo: 'success', mensagem: 'Regra atualizada.' };
} catch (erro: unknown) {
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
}
}}
>
Editar
</button>
<button
type="button"
class="btn btn-xs btn-error"
onclick={async () => {
if (!confirm('Tem certeza que deseja excluir esta regra?')) return;
try {
await client.mutation(api.security.deletarRegraPorta, {
regraId: regra._id,
usuarioId: obterUsuarioId()
});
feedback = { tipo: 'success', mensagem: 'Regra excluída.' };
} catch (erro: unknown) {
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
}
}}
>
Excluir
</button>
</div>
</div>
{/each}
{/if}
@@ -683,13 +985,21 @@ const chartPaths = $derived.by(() => {
<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} />
<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} />
<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} />
@@ -704,6 +1014,276 @@ const chartPaths = $derived.by(() => {
</div>
</div>
</section>
<!-- Seção de Rate Limiting -->
<section class="border-warning/20 bg-base-100/80 rounded-3xl border p-6 shadow-2xl">
<div class="mb-4 flex items-center justify-between">
<div>
<h3 class="text-warning text-2xl font-bold">Rate Limiting Avançado</h3>
<p class="text-base-content/70 text-sm">
Configure limites de requisições por IP, usuário, endpoint ou globalmente para proteger o
sistema.
</p>
</div>
<button
type="button"
class="btn btn-warning btn-sm"
onclick={() => {
resetarFormRateLimit();
mostrarRateLimitConfig = !mostrarRateLimitConfig;
}}
>
{mostrarRateLimitConfig ? 'Cancelar' : '+ Nova Configuração'}
</button>
</div>
{#if mostrarRateLimitConfig}
<form
class="border-warning/30 bg-base-200/40 mb-6 space-y-4 rounded-xl border p-4"
onsubmit={salvarConfigRateLimit}
>
<div class="grid gap-4 md:grid-cols-2">
<label class="form-control">
<div class="label">
<span class="label-text font-semibold">Nome da Configuração</span>
</div>
<input
type="text"
class="input input-bordered"
placeholder="Ex: Limite IPs suspeitos"
bind:value={rateLimitNome}
required
/>
</label>
<label class="form-control">
<div class="label">
<span class="label-text font-semibold">Tipo</span>
</div>
<select class="select select-bordered" bind:value={rateLimitTipo}>
<option value="ip">Por IP</option>
<option value="usuario">Por Usuário</option>
<option value="endpoint">Por Endpoint</option>
<option value="global">Global</option>
</select>
</label>
{#if rateLimitTipo !== 'global'}
<label class="form-control">
<div class="label">
<span class="label-text font-semibold">
Identificador ({rateLimitTipo === 'ip'
? 'IP'
: rateLimitTipo === 'usuario'
? 'ID do Usuário'
: 'Caminho do Endpoint'})
</span>
</div>
<input
type="text"
class="input input-bordered"
placeholder={rateLimitTipo === 'ip'
? 'Ex: 192.168.1.1'
: rateLimitTipo === 'usuario'
? 'ID do usuário'
: '/api/security/registrar'}
bind:value={rateLimitIdentificador}
required
/>
</label>
{/if}
<label class="form-control">
<div class="label">
<span class="label-text font-semibold">Limite de Requisições</span>
</div>
<input
type="number"
class="input input-bordered"
min="1"
max="10000"
bind:value={rateLimitLimite}
required
/>
</label>
<label class="form-control">
<div class="label">
<span class="label-text font-semibold">Janela de Tempo (segundos)</span>
</div>
<input
type="number"
class="input input-bordered"
min="1"
max="86400"
bind:value={rateLimitJanelaSegundos}
required
/>
</label>
<label class="form-control">
<div class="label">
<span class="label-text font-semibold">Estratégia</span>
</div>
<select class="select select-bordered" bind:value={rateLimitEstrategia}>
<option value="fixed_window">Fixed Window</option>
<option value="sliding_window">Sliding Window</option>
<option value="token_bucket">Token Bucket</option>
</select>
</label>
<label class="form-control">
<div class="label">
<span class="label-text font-semibold">Ação ao Exceder</span>
</div>
<select class="select select-bordered" bind:value={rateLimitAcaoExcedido}>
<option value="bloquear">Bloquear</option>
<option value="throttle">Throttle (Atrasar)</option>
<option value="alertar">Apenas Alertar</option>
</select>
</label>
<label class="form-control">
<div class="label">
<span class="label-text font-semibold">Bloqueio Temporário (segundos, opcional)</span>
</div>
<input
type="number"
class="input input-bordered"
min="0"
placeholder="Deixe vazio para sem bloqueio temporário"
bind:value={rateLimitBloqueioTemporarioSegundos}
/>
</label>
<label class="form-control">
<div class="label">
<span class="label-text font-semibold">Prioridade</span>
</div>
<input
type="number"
class="input input-bordered"
min="0"
max="100"
bind:value={rateLimitPrioridade}
/>
<div class="label">
<span class="label-text-alt">Maior prioridade = aplicado primeiro</span>
</div>
</label>
</div>
<label class="form-control">
<div class="label">
<span class="label-text font-semibold">Notas</span>
</div>
<textarea
class="textarea textarea-bordered"
placeholder="Observações sobre esta configuração..."
bind:value={rateLimitNotas}
></textarea>
</label>
<div class="flex gap-2">
<button type="submit" class="btn btn-warning flex-1">
{rateLimitEditando ? 'Atualizar Configuração' : 'Criar Configuração'}
</button>
{#if rateLimitEditando}
<button
type="button"
class="btn btn-ghost"
onclick={() => {
resetarFormRateLimit();
mostrarRateLimitConfig = false;
}}
>
Cancelar
</button>
{/if}
</div>
</form>
{/if}
<div class="space-y-3">
<h4 class="text-warning text-lg font-bold">Configurações Ativas</h4>
{#if configsRateLimit?.data && configsRateLimit.data.length > 0}
<div class="space-y-2">
{#each configsRateLimit.data as config (config._id)}
<div class="border-warning/30 bg-base-200/40 rounded-xl border p-4">
<div class="flex items-start justify-between">
<div class="flex-1">
<div class="flex items-center gap-2">
<h5 class="font-bold">{config.nome}</h5>
<span class="badge badge-warning badge-sm">{config.tipo}</span>
{#if config.ativo}
<span class="badge badge-success badge-sm">Ativo</span>
{:else}
<span class="badge badge-ghost badge-sm">Inativo</span>
{/if}
</div>
<div class="mt-2 grid gap-1 text-sm">
<p>
<span class="font-semibold">Limite:</span>
{config.limite} requisições em {config.janelaSegundos}s
</p>
<p>
<span class="font-semibold">Estratégia:</span>
{config.estrategia.replace('_', ' ')}
</p>
<p>
<span class="font-semibold">Ação:</span>
{config.acaoExcedido}
</p>
{#if config.identificador}
<p>
<span class="font-semibold">Identificador:</span>
{config.identificador}
</p>
{/if}
<p>
<span class="font-semibold">Prioridade:</span>
{config.prioridade}
</p>
{#if config.notas}
<p class="text-base-content/70">
<span class="font-semibold">Notas:</span>
{config.notas}
</p>
{/if}
</div>
</div>
<div class="flex flex-col gap-2">
<button
type="button"
class="btn btn-xs btn-outline"
onclick={() => toggleAtivoRateLimit(config._id, config.ativo)}
>
{config.ativo ? 'Desativar' : 'Ativar'}
</button>
<button
type="button"
class="btn btn-xs btn-outline"
onclick={() => editarConfigRateLimit(config._id)}
>
Editar
</button>
<button
type="button"
class="btn btn-xs btn-error btn-outline"
onclick={() => deletarConfigRateLimit(config._id)}
>
Deletar
</button>
</div>
</div>
</div>
{/each}
</div>
{:else}
<p class="text-base-content/60 text-sm">Nenhuma configuração de rate limit cadastrada.</p>
{/if}
</div>
</section>
</div>
<style>
@@ -711,4 +1291,3 @@ const chartPaths = $derived.by(() => {
display: none;
}
</style>

View File

@@ -305,16 +305,19 @@
(() => {
const agregados = new SvelteMap<number, SolicitacoesPorAnoResumo>();
for (const periodo of solicitacoesAprovadas) {
const totalDias = periodo.diasFerias;
const existente = agregados.get(periodo.anoReferencia) ?? {
ano: periodo.anoReferencia,
for (const solicitacao of solicitacoesAprovadas) {
const totalDias = solicitacao.periodos.reduce(
(acc, periodo) => acc + periodo.diasFerias,
0
);
const existente = agregados.get(solicitacao.anoReferencia) ?? {
ano: solicitacao.anoReferencia,
solicitacoes: 0,
diasTotais: 0
};
existente.solicitacoes += 1;
existente.diasTotais += totalDias;
agregados.set(periodo.anoReferencia, existente);
agregados.set(solicitacao.anoReferencia, existente);
}
return Array.from(agregados.values()).sort((a, b) => a.ano - b.ano);
@@ -458,18 +461,9 @@
console.log('📅 [Eventos] Total de eventos:', eventosFerias.length);
console.log('📋 [Periodos] Total de períodos:', periodosDetalhados.length);
console.log('✅ [Aprovadas] Total de solicitações aprovadas:', solicitacoesAprovadas.length);
console.log('📊 [PeriodosPorMes] Total:', periodosPorMes.length);
console.log('📊 [PeriodosPorMesAtivos] Total:', periodosPorMesAtivos.length);
console.log('📊 [SolicitacoesPorAno] Total:', solicitacoesPorAno.length);
if (eventosFerias.length > 0) {
console.log('📅 [Eventos] Primeiro evento:', eventosFerias[0]);
}
if (periodosPorMes.length > 0) {
console.log('📊 [PeriodosPorMes] Primeiro:', periodosPorMes[0]);
}
if (solicitacoesPorAno.length > 0) {
console.log('📊 [SolicitacoesPorAno] Primeiro:', solicitacoesPorAno[0]);
}
});
let calendarioContainer: HTMLDivElement | null = null;