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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user