feat: Introduce structured table definitions in convex/tables for various entities and remove the todos example table.

This commit is contained in:
2025-12-02 09:55:07 -03:00
parent 1c0bd219b2
commit 05e7f1181d
30 changed files with 2700 additions and 2535 deletions

View File

@@ -1,16 +1,12 @@
import { v } from 'convex/values';
import {
internalMutation,
mutation,
query
} from './_generated/server';
import { internalMutation, mutation, query } from './_generated/server';
import { internal } from './_generated/api';
import type { Id } from './_generated/dataModel';
import type {
AtaqueCiberneticoTipo,
SeveridadeSeguranca,
StatusEventoSeguranca
} from './schema';
} from './tables/security';
import type { MutationCtx, QueryCtx } from './_generated/server';
import { RateLimiter, SECOND } from '@convex-dev/rate-limiter';
import { components } from './_generated/api';
@@ -413,9 +409,9 @@ const acaoOrigemValidator = v.union(v.literal('automatico'), v.literal('manual')
// Função para analisar string e detectar ataques
function analisarStringParaAtaques(texto: string): AtaqueCiberneticoTipo | null {
if (!texto) return null;
const textoLower = texto.toLowerCase();
// Verificar cada tipo de ataque em ordem de prioridade
for (const tipo of ATAQUES_PRIORITARIOS) {
const patterns = KEYWORDS[tipo];
@@ -423,7 +419,7 @@ function analisarStringParaAtaques(texto: string): AtaqueCiberneticoTipo | null
return tipo;
}
}
return null;
}
@@ -591,14 +587,23 @@ export const registrarEventoSeguranca = mutation({
handler: async (ctx, args) => {
// Aplicar rate limiting por IP se fornecido
if (args.origemIp) {
const rateLimitResult = await aplicarRateLimit(ctx, 'ip', args.origemIp, 'registrarEventoSeguranca');
const rateLimitResult = await aplicarRateLimit(
ctx,
'ip',
args.origemIp,
'registrarEventoSeguranca'
);
if (!rateLimitResult.permitido) {
throw new Error(rateLimitResult.motivo ?? 'Rate limit excedido');
}
}
const tipo = inferirTipoAtaque(args);
const severidade = calcularSeveridade(tipo, args.metricas ?? undefined, args.severidade ?? undefined);
const severidade = calcularSeveridade(
tipo,
args.metricas ?? undefined,
args.severidade ?? undefined
);
const status = statusInicial(severidade);
const duplicado = await ctx.db
@@ -727,10 +732,18 @@ export const listarEventosSeguranca = query({
const candidatos = await builder.order('desc').take(limit * 3);
const filtrados = candidatos
.filter((evento) => {
if (args.severidades && args.severidades.length > 0 && !args.severidades.includes(evento.severidade)) {
if (
args.severidades &&
args.severidades.length > 0 &&
!args.severidades.includes(evento.severidade)
) {
return false;
}
if (args.tiposAtaque && args.tiposAtaque.length > 0 && !args.tiposAtaque.includes(evento.tipoAtaque)) {
if (
args.tiposAtaque &&
args.tiposAtaque.length > 0 &&
!args.tiposAtaque.includes(evento.tipoAtaque)
) {
return false;
}
if (args.status && args.status.length > 0 && !args.status.includes(evento.status)) {
@@ -829,10 +842,7 @@ export const obterVisaoCamadas = query({
for (const evento of eventos) {
const idx = Math.min(
bucketCount - 1,
Math.max(
0,
Math.floor((evento.timestamp - inicioJanela) / bucketSize)
)
Math.max(0, Math.floor((evento.timestamp - inicioJanela) / bucketSize))
);
const bucket = series[idx];
if (evento.severidade === 'critico') criticos += 1;
@@ -893,11 +903,12 @@ export const listarReputacoes = query({
handler: async (ctx, args) => {
const limit = args.limit && args.limit > 0 ? Math.min(args.limit, 500) : 200;
const builder = args.lista === undefined
? ctx.db.query('ipReputation')
: args.lista === 'blacklist'
? ctx.db.query('ipReputation').withIndex('by_blacklist', (q) => q.eq('blacklist', true))
: ctx.db.query('ipReputation').withIndex('by_whitelist', (q) => q.eq('whitelist', true));
const builder =
args.lista === undefined
? ctx.db.query('ipReputation')
: args.lista === 'blacklist'
? ctx.db.query('ipReputation').withIndex('by_blacklist', (q) => q.eq('blacklist', true))
: ctx.db.query('ipReputation').withIndex('by_whitelist', (q) => q.eq('whitelist', true));
const docs = await builder.order('desc').take(limit * 2);
const filtrados = docs
@@ -945,7 +956,12 @@ export const atualizarReputacaoIndicador = mutation({
}),
handler: async (ctx, args) => {
// Aplicar rate limiting por usuário
const rateLimitResult = await aplicarRateLimit(ctx, 'usuario', args.usuarioId, 'atualizarReputacaoIndicador');
const rateLimitResult = await aplicarRateLimit(
ctx,
'usuario',
args.usuarioId,
'atualizarReputacaoIndicador'
);
if (!rateLimitResult.permitido) {
throw new Error(rateLimitResult.motivo ?? 'Rate limit excedido');
}
@@ -1075,7 +1091,8 @@ export const configurarRegraPorta = mutation({
}),
handler: async (ctx, args) => {
const agora = Date.now();
const expiraEm = args.temporario && args.duracaoSegundos ? agora + args.duracaoSegundos * 1000 : undefined;
const expiraEm =
args.temporario && args.duracaoSegundos ? agora + args.duracaoSegundos * 1000 : undefined;
if (args.regraId) {
await ctx.db.patch(args.regraId, {
@@ -1172,7 +1189,14 @@ export const registrarAcaoIncidente = mutation({
tipo: acaoIncidenteValidator,
origem: acaoOrigemValidator,
executadoPor: v.optional(v.id('usuarios')),
status: v.optional(v.union(v.literal('pendente'), v.literal('executando'), v.literal('concluido'), v.literal('falhou'))),
status: v.optional(
v.union(
v.literal('pendente'),
v.literal('executando'),
v.literal('concluido'),
v.literal('falhou')
)
),
detalhes: v.optional(v.string()),
resultado: v.optional(v.string()),
relacionadoA: v.optional(v.id('ipReputation'))
@@ -1338,9 +1362,7 @@ export const processarRelatorioSegurancaInternal = internalMutation({
const eventos = await ctx.db
.query('securityEvents')
.withIndex('by_timestamp', (q) =>
q
.gte('timestamp', relatorio.filtros.dataInicio)
.lte('timestamp', relatorio.filtros.dataFim)
q.gte('timestamp', relatorio.filtros.dataInicio).lte('timestamp', relatorio.filtros.dataFim)
)
.collect();
@@ -1350,14 +1372,14 @@ export const processarRelatorioSegurancaInternal = internalMutation({
relatorio.filtros.severidades.length > 0 &&
!relatorio.filtros.severidades.includes(evento.severidade)
) {
return false;
return false;
}
if (
relatorio.filtros.tiposAtaque &&
relatorio.filtros.tiposAtaque.length > 0 &&
!relatorio.filtros.tiposAtaque.includes(evento.tipoAtaque)
) {
return false;
return false;
}
return true;
});
@@ -1509,7 +1531,10 @@ export const dispararAlertasInternos = internalMutation({
const usuariosNotificados: Id<'usuarios'>[] = [];
for (const role of rolesTi) {
const membros = await ctx.db.query('usuarios').withIndex('by_role', (q) => q.eq('roleId', role._id)).collect();
const membros = await ctx.db
.query('usuarios')
.withIndex('by_role', (q) => q.eq('roleId', role._id))
.collect();
for (const usuario of membros) {
usuariosNotificados.push(usuario._id);
}
@@ -1602,7 +1627,9 @@ async function aplicarRateLimit(
): Promise<{ permitido: boolean; motivo?: string; retryAfter?: number }> {
const configs = await ctx.db
.query('rateLimitConfig')
.withIndex('by_tipo_identificador', (q) => q.eq('tipo', tipo).eq('identificador', identificador))
.withIndex('by_tipo_identificador', (q) =>
q.eq('tipo', tipo).eq('identificador', identificador)
)
.filter((q) => q.eq(q.field('ativo'), true))
.collect();
@@ -1611,7 +1638,9 @@ async function aplicarRateLimit(
// Verificar configuração global
const globalConfigs = await ctx.db
.query('rateLimitConfig')
.withIndex('by_tipo_identificador', (q) => q.eq('tipo', 'global').eq('identificador', 'global'))
.withIndex('by_tipo_identificador', (q) =>
q.eq('tipo', 'global').eq('identificador', 'global')
)
.filter((q) => q.eq(q.field('ativo'), true))
.collect();
@@ -1626,10 +1655,11 @@ async function aplicarRateLimit(
// Converter janelaSegundos para período do rate-limiter
const periodo = config.janelaSegundos * SECOND;
// Determinar estratégia baseada na configuração
// O rate-limiter suporta apenas 'token bucket' e 'fixed window'
const kind: 'token bucket' | 'fixed window' = config.estrategia === 'token_bucket' ? 'token bucket' : 'fixed window';
const kind: 'token bucket' | 'fixed window' =
config.estrategia === 'token_bucket' ? 'token bucket' : 'fixed window';
// Criar namespace único para este rate limit
const namespace = `${tipo}:${identificador}:${endpoint ?? 'default'}`;
@@ -1643,7 +1673,10 @@ async function aplicarRateLimit(
period: periodo,
...(config.estrategia === 'token_bucket' ? { capacity: config.limite } : {})
}
} as Record<string, { kind: 'token bucket' | 'fixed window'; rate: number; period: number; capacity?: number }>;
} as Record<
string,
{ kind: 'token bucket' | 'fixed window'; rate: number; period: number; capacity?: number }
>;
const rateLimiter = new RateLimiter(components.rateLimiter, rateLimiterConfig);
@@ -1654,7 +1687,7 @@ async function aplicarRateLimit(
if (!result.ok) {
const retryAfter = result.retryAfter ?? periodo;
if (config.acaoExcedido === 'bloquear') {
return {
permitido: false,
@@ -1688,7 +1721,12 @@ export const criarConfigRateLimit = mutation({
args: {
usuarioId: v.id('usuarios'),
nome: v.string(),
tipo: v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global')),
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(),
@@ -1737,13 +1775,11 @@ export const atualizarConfigRateLimit = mutation({
limite: v.optional(v.number()),
janelaSegundos: v.optional(v.number()),
estrategia: v.optional(
v.union(
v.literal('fixed_window'),
v.literal('sliding_window'),
v.literal('token_bucket')
)
v.union(v.literal('fixed_window'), v.literal('sliding_window'), v.literal('token_bucket'))
),
acaoExcedido: v.optional(
v.union(v.literal('bloquear'), v.literal('throttle'), v.literal('alertar'))
),
acaoExcedido: v.optional(v.union(v.literal('bloquear'), v.literal('throttle'), v.literal('alertar'))),
bloqueioTemporarioSegundos: v.optional(v.number()),
ativo: v.optional(v.boolean()),
prioridade: v.optional(v.number()),
@@ -1794,7 +1830,9 @@ export const atualizarConfigRateLimit = mutation({
export const listarConfigsRateLimit = query({
args: {
tipo: v.optional(v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global'))),
tipo: v.optional(
v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global'))
),
ativo: v.optional(v.boolean()),
limit: v.optional(v.number())
},
@@ -1802,7 +1840,12 @@ export const listarConfigsRateLimit = query({
v.object({
_id: v.id('rateLimitConfig'),
nome: v.string(),
tipo: v.union(v.literal('ip'), v.literal('usuario'), v.literal('endpoint'), v.literal('global')),
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(),
@@ -1882,8 +1925,12 @@ export const analisarRequisicaoHTTP = mutation({
args.url,
args.method,
args.body ?? '',
Object.entries(args.queryParams ?? {}).map(([k, v]) => `${k}=${v}`).join('&'),
Object.entries(args.headers ?? {}).map(([k, v]) => `${k}:${v}`).join('\n'),
Object.entries(args.queryParams ?? {})
.map(([k, v]) => `${k}=${v}`)
.join('&'),
Object.entries(args.headers ?? {})
.map(([k, v]) => `${k}:${v}`)
.join('\n'),
args.userAgent ?? ''
].join('\n');
@@ -1904,7 +1951,8 @@ export const analisarRequisicaoHTTP = mutation({
// Permitir que o chamador informe o destino/protocolo via query string em cenários de dev/teste
const destinoIp =
(args.queryParams && (args.queryParams['dst'] || args.queryParams['dest'] || args.queryParams['destino'])) ||
(args.queryParams &&
(args.queryParams['dst'] || args.queryParams['dest'] || args.queryParams['destino'])) ||
undefined;
const protocolo = (args.queryParams && (args.queryParams['proto'] as string)) || 'http';
@@ -1922,9 +1970,11 @@ export const analisarRequisicaoHTTP = mutation({
protocolo,
transporte: 'tcp',
detectadoPor: 'analisador_http_automatico',
fingerprint: args.userAgent ? {
userAgent: args.userAgent
} : undefined,
fingerprint: args.userAgent
? {
userAgent: args.userAgent
}
: undefined,
destinoIp: destinoIp ?? undefined,
tags: ['detecção_automática', 'http', tipoAtaque],
atualizadoEm: agora
@@ -1978,19 +2028,13 @@ export const detectarBruteForce = internalMutation({
tentativasFalhas = await ctx.db
.query('logsLogin')
.withIndex('by_ip', (q) => q.eq('ipAddress', args.ipAddress))
.filter((q) =>
q.gte(q.field('timestamp'), dataLimite) &&
q.eq(q.field('sucesso'), false)
)
.filter((q) => q.gte(q.field('timestamp'), dataLimite) && q.eq(q.field('sucesso'), false))
.collect();
} else if (args.usuarioId) {
tentativasFalhas = await ctx.db
.query('logsLogin')
.withIndex('by_usuario', (q) => q.eq('usuarioId', args.usuarioId))
.filter((q) =>
q.gte(q.field('timestamp'), dataLimite) &&
q.eq(q.field('sucesso'), false)
)
.filter((q) => q.gte(q.field('timestamp'), dataLimite) && q.eq(q.field('sucesso'), false))
.collect();
} else {
// Buscar todas as tentativas falhas na janela
@@ -2026,7 +2070,8 @@ export const detectarBruteForce = internalMutation({
const eventosIds: Id<'securityEvents'>[] = [];
for (const { ip, count } of ipsSuspeitos) {
const severidade: SeveridadeSeguranca = count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo';
const severidade: SeveridadeSeguranca =
count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo';
const referencia = `brute_force_${ip}_${Date.now()}`;
const agora = Date.now();
@@ -2058,10 +2103,12 @@ export const detectarBruteForce = internalMutation({
'ip',
delta,
severidade,
severidade === 'alto' ? {
blacklist: true,
bloqueadoAte: agora + (60 * 60 * 1000) // Bloquear por 1 hora
} : undefined
severidade === 'alto'
? {
blacklist: true,
bloqueadoAte: agora + 60 * 60 * 1000 // Bloquear por 1 hora
}
: undefined
);
}
@@ -2109,13 +2156,7 @@ export const criarEventosTeste = mutation({
];
// IPs de teste
const ipsTeste = [
'192.168.1.100',
'10.0.0.50',
'172.16.0.25',
'203.0.113.42',
'198.51.100.15'
];
const ipsTeste = ['192.168.1.100', '10.0.0.50', '172.16.0.25', '203.0.113.42', '198.51.100.15'];
for (let i = 0; i < quantidade; i++) {
const tipoAtaque = tiposAtaque[i % tiposAtaque.length];
@@ -2124,7 +2165,7 @@ export const criarEventosTeste = mutation({
const eventoId = await ctx.db.insert('securityEvents', {
referencia,
timestamp: agora - (i * 60000), // Espaçar eventos em 1 minuto
timestamp: agora - i * 60000, // Espaçar eventos em 1 minuto
tipoAtaque: tipoAtaque.tipo,
severidade: tipoAtaque.severidade,
status: statusInicial(tipoAtaque.severidade),
@@ -2140,7 +2181,7 @@ export const criarEventosTeste = mutation({
pps: Math.floor(Math.random() * 50000)
},
tags: ['teste', 'validação', tipoAtaque.tipo],
atualizadoEm: agora - (i * 60000)
atualizadoEm: agora - i * 60000
});
eventosIds.push(eventoId);
@@ -2153,9 +2194,11 @@ export const criarEventosTeste = mutation({
'ip',
delta,
tipoAtaque.severidade,
tipoAtaque.severidade === 'critico' || tipoAtaque.severidade === 'alto' ? {
blacklist: true
} : undefined
tipoAtaque.severidade === 'critico' || tipoAtaque.severidade === 'alto'
? {
blacklist: true
}
: undefined
);
}
@@ -2208,7 +2251,8 @@ export const monitorarLogsLogin = internalMutation({
// Registrar eventos para cada IP suspeito
let ipsBloqueados = 0;
for (const { ip, count } of ipsSuspeitos) {
const severidade: SeveridadeSeguranca = count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo';
const severidade: SeveridadeSeguranca =
count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo';
const referencia = `brute_force_${ip}_${Date.now()}`;
const agora = Date.now();
@@ -2238,10 +2282,12 @@ export const monitorarLogsLogin = internalMutation({
'ip',
delta,
severidade,
severidade === 'alto' ? {
blacklist: true,
bloqueadoAte: agora + (60 * 60 * 1000)
} : undefined
severidade === 'alto'
? {
blacklist: true,
bloqueadoAte: agora + 60 * 60 * 1000
}
: undefined
);
if (severidade === 'alto') {
@@ -2302,7 +2348,12 @@ export const seedRateLimitDev = mutation({
const existing = await ctx.db
.query('rateLimitConfig')
.withIndex('by_tipo_identificador', (q) =>
q.eq('tipo', params.tipo).eq('identificador', params.identificador ?? (params.tipo === 'global' ? 'global' : undefined)),
q
.eq('tipo', params.tipo)
.eq(
'identificador',
params.identificador ?? (params.tipo === 'global' ? 'global' : undefined)
)
)
.collect();
const agora = Date.now();
@@ -2315,9 +2366,9 @@ export const seedRateLimitDev = mutation({
estrategia: params.estrategia,
acaoExcedido: params.acaoExcedido,
ativo: true,
prioridade: params.prioridade ?? (doc.prioridade ?? 0),
prioridade: params.prioridade ?? doc.prioridade ?? 0,
atualizadoEm: agora,
notas: params.notas,
notas: params.notas
});
} else {
await ctx.db.insert('rateLimitConfig', {
@@ -2420,4 +2471,3 @@ export const deletarRegraPorta = mutation({
return null;
}
});