Merge remote-tracking branch 'origin' into feat-pedidos
This commit is contained in:
@@ -1,9 +1,8 @@
|
||||
import { RateLimiter, SECOND } from '@convex-dev/rate-limiter';
|
||||
import { v } from 'convex/values';
|
||||
import { components, internal } from './_generated/api';
|
||||
import { internalMutation, mutation, MutationCtx, query, QueryCtx } from './_generated/server';
|
||||
import { internal, api, components } from './_generated/api';
|
||||
import type { Id } from './_generated/dataModel';
|
||||
import type { MutationCtx, QueryCtx } from './_generated/server';
|
||||
import { internalMutation, mutation, query } from './_generated/server';
|
||||
import type {
|
||||
AtaqueCiberneticoTipo,
|
||||
SeveridadeSeguranca,
|
||||
@@ -1435,6 +1434,7 @@ export const listarAlertConfigs = query({
|
||||
severidadeMin: severidadeValidator,
|
||||
tiposAtaque: v.optional(v.array(ataqueValidator)),
|
||||
reenvioMin: v.number(),
|
||||
templateCodigo: v.optional(v.string()),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number()
|
||||
})
|
||||
@@ -1455,6 +1455,7 @@ export const listarAlertConfigs = query({
|
||||
severidadeMin: r.severidadeMin,
|
||||
tiposAtaque: r.tiposAtaque,
|
||||
reenvioMin: r.reenvioMin,
|
||||
templateCodigo: r.templateCodigo,
|
||||
criadoEm: r.criadoEm,
|
||||
atualizadoEm: r.atualizadoEm
|
||||
}));
|
||||
@@ -1471,6 +1472,7 @@ export const salvarAlertConfig = mutation({
|
||||
severidadeMin: severidadeValidator,
|
||||
tiposAtaque: v.optional(v.array(ataqueValidator)),
|
||||
reenvioMin: v.number(),
|
||||
templateCodigo: v.optional(v.string()), // Template a ser usado
|
||||
criadoPor: v.id('usuarios')
|
||||
},
|
||||
returns: v.object({ _id: v.id('alertConfigs') }),
|
||||
@@ -1485,6 +1487,7 @@ export const salvarAlertConfig = mutation({
|
||||
severidadeMin: args.severidadeMin,
|
||||
tiposAtaque: args.tiposAtaque,
|
||||
reenvioMin: args.reenvioMin,
|
||||
templateCodigo: args.templateCodigo,
|
||||
atualizadoEm: agora
|
||||
});
|
||||
return { _id: args.configId };
|
||||
@@ -1497,6 +1500,7 @@ export const salvarAlertConfig = mutation({
|
||||
severidadeMin: args.severidadeMin,
|
||||
tiposAtaque: args.tiposAtaque,
|
||||
reenvioMin: args.reenvioMin,
|
||||
templateCodigo: args.templateCodigo ?? 'incidente_critico', // Padrão
|
||||
criadoPor: args.criadoPor,
|
||||
criadoEm: agora,
|
||||
atualizadoEm: agora
|
||||
@@ -1522,6 +1526,197 @@ export const dispararAlertasInternos = internalMutation({
|
||||
const evento = await ctx.db.get(args.eventoId);
|
||||
if (!evento) return null;
|
||||
|
||||
// Buscar todas as configurações de alerta ativas
|
||||
const alertConfigs = await ctx.db.query('alertConfigs').collect();
|
||||
|
||||
// Obter URL do sistema
|
||||
let urlSistema = process.env.SITE_URL || 'http://localhost:5173';
|
||||
if (!urlSistema.match(/^https?:\/\//i)) {
|
||||
urlSistema = `http://${urlSistema}`;
|
||||
}
|
||||
|
||||
// Formatar data/hora
|
||||
const dataHora = new Date(evento.timestamp).toLocaleString('pt-BR', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
// Mapear severidade para texto legível
|
||||
const severityLabels: Record<SeveridadeSeguranca, string> = {
|
||||
informativo: 'Informativo',
|
||||
baixo: 'Baixo',
|
||||
moderado: 'Moderado',
|
||||
alto: 'Alto',
|
||||
critico: 'Crítico'
|
||||
};
|
||||
|
||||
// Mapear tipo de ataque para texto legível
|
||||
const attackLabels: Record<string, string> = {
|
||||
phishing: 'Phishing',
|
||||
malware: 'Malware',
|
||||
ransomware: 'Ransomware',
|
||||
brute_force: 'Brute Force',
|
||||
credential_stuffing: 'Credential Stuffing',
|
||||
sql_injection: 'SQL Injection',
|
||||
xss: 'XSS',
|
||||
path_traversal: 'Path Traversal',
|
||||
command_injection: 'Command Injection',
|
||||
nosql_injection: 'NoSQL Injection',
|
||||
xxe: 'XXE',
|
||||
man_in_the_middle: 'MITM',
|
||||
ddos: 'DDoS',
|
||||
engenharia_social: 'Engenharia Social',
|
||||
cve_exploit: 'Exploração de CVE',
|
||||
apt: 'APT',
|
||||
zero_day: 'Zero-Day',
|
||||
supply_chain: 'Supply Chain',
|
||||
fileless_malware: 'Fileless Malware',
|
||||
polymorphic_malware: 'Polymorphic',
|
||||
ransomware_lateral: 'Ransomware Lateral',
|
||||
deepfake_phishing: 'Deepfake Phishing',
|
||||
adversarial_ai: 'Ataque IA',
|
||||
side_channel: 'Side-Channel',
|
||||
firmware_bootloader: 'Firmware/Bootloader',
|
||||
bec: 'BEC',
|
||||
botnet: 'Botnet',
|
||||
ot_ics: 'OT/ICS',
|
||||
quantum_attack: 'Quantum'
|
||||
};
|
||||
|
||||
const tipoAtaqueLabel = attackLabels[evento.tipoAtaque] || evento.tipoAtaque.replace(/_/g, ' ');
|
||||
const severidadeLabel = severityLabels[evento.severidade] || evento.severidade;
|
||||
|
||||
// Função auxiliar para verificar se a severidade atende ao mínimo
|
||||
const severidadeAtende = (
|
||||
severidade: SeveridadeSeguranca,
|
||||
min: SeveridadeSeguranca
|
||||
): boolean => {
|
||||
const ordem: SeveridadeSeguranca[] = ['informativo', 'baixo', 'moderado', 'alto', 'critico'];
|
||||
return ordem.indexOf(severidade) >= ordem.indexOf(min);
|
||||
};
|
||||
|
||||
// Processar cada configuração de alerta
|
||||
for (const config of alertConfigs) {
|
||||
// Verificar se a severidade atende ao mínimo
|
||||
if (!severidadeAtende(evento.severidade, config.severidadeMin)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verificar se o tipo de ataque está na lista (se especificado)
|
||||
if (config.tiposAtaque && config.tiposAtaque.length > 0) {
|
||||
if (!config.tiposAtaque.includes(evento.tipoAtaque)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Buscar usuário sistema para enviar emails (ou usar o primeiro usuário TI)
|
||||
const rolesTi = await ctx.db
|
||||
.query('roles')
|
||||
.filter((q) => q.eq(q.field('admin'), true))
|
||||
.collect();
|
||||
let usuarioSistema: Id<'usuarios'> | undefined;
|
||||
if (rolesTi) {
|
||||
const usuarioTi = await ctx.db
|
||||
.query('usuarios')
|
||||
.withIndex('by_role', (q) => q.eq('roleId', rolesTi[0]._id))
|
||||
.first();
|
||||
if (usuarioTi) {
|
||||
usuarioSistema = usuarioTi._id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!usuarioSistema) {
|
||||
console.error('❌ Não foi possível encontrar usuário sistema para enviar alertas');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Preparar variáveis do template
|
||||
const variaveisTemplate = {
|
||||
destinatarioNome: '', // Será preenchido por destinatário
|
||||
tipoAtaque: tipoAtaqueLabel,
|
||||
severidade: severidadeLabel,
|
||||
descricao: evento.descricao,
|
||||
origemIp: evento.origemIp || 'N/A',
|
||||
dataHora,
|
||||
urlSistema
|
||||
};
|
||||
|
||||
// ENVIAR EMAILS
|
||||
if (config.canais.email && config.emails.length > 0) {
|
||||
const templateCodigo = config.templateCodigo || 'incidente_critico';
|
||||
|
||||
for (const emailDestinatario of config.emails) {
|
||||
// Buscar usuário pelo email
|
||||
const usuarioDestinatario = await ctx.db
|
||||
.query('usuarios')
|
||||
.filter((q) => q.eq(q.field('email'), emailDestinatario))
|
||||
.first();
|
||||
|
||||
if (usuarioDestinatario) {
|
||||
variaveisTemplate.destinatarioNome = usuarioDestinatario.nome;
|
||||
|
||||
// Enviar email usando template
|
||||
ctx.scheduler
|
||||
.runAfter(0, api.email.enviarEmailComTemplate, {
|
||||
destinatario: emailDestinatario,
|
||||
destinatarioId: usuarioDestinatario._id,
|
||||
templateCodigo,
|
||||
variaveis: variaveisTemplate,
|
||||
enviadoPor: usuarioSistema
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Erro ao agendar email de alerta para ${emailDestinatario}:`, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ENVIAR CHAT
|
||||
if (config.canais.chat && config.chatUsers.length > 0) {
|
||||
const templateCodigo = config.templateCodigo || 'incidente_critico';
|
||||
|
||||
// Buscar template para chat
|
||||
const template = await ctx.runQuery(api.templatesMensagens.obterTemplatePorCodigo, {
|
||||
codigo: templateCodigo
|
||||
});
|
||||
|
||||
if (template) {
|
||||
// Importar função de renderização
|
||||
const { renderizarTemplateChatFromDoc } = await import('./templatesMensagens');
|
||||
|
||||
for (const chatUserEmail of config.chatUsers) {
|
||||
// Buscar usuário pelo email
|
||||
const usuarioDestinatario = await ctx.db
|
||||
.query('usuarios')
|
||||
.filter((q) => q.eq(q.field('email'), chatUserEmail))
|
||||
.first();
|
||||
|
||||
if (usuarioDestinatario && usuarioSistema) {
|
||||
variaveisTemplate.destinatarioNome = usuarioDestinatario.nome;
|
||||
|
||||
// Renderizar mensagem do template
|
||||
const mensagemChat = renderizarTemplateChatFromDoc(template, variaveisTemplate);
|
||||
|
||||
// Usar função interna para criar conversa e enviar mensagem
|
||||
ctx.scheduler
|
||||
.runAfter(0, internal.security.enviarMensagemChatSistema, {
|
||||
usuarioSistemaId: usuarioSistema,
|
||||
usuarioDestinatarioId: usuarioDestinatario._id,
|
||||
mensagem: mensagemChat
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`Erro ao agendar mensagem de chat para ${chatUserEmail}:`, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Manter notificação padrão para usuários TI (compatibilidade)
|
||||
const rolesTi = await ctx.db
|
||||
.query('roles')
|
||||
.filter((q) => q.eq(q.field('admin'), true))
|
||||
@@ -1546,7 +1741,7 @@ export const dispararAlertasInternos = internalMutation({
|
||||
conversaId: undefined,
|
||||
mensagemId: undefined,
|
||||
remetenteId: undefined,
|
||||
titulo: `🚨 ${evento.severidade.toUpperCase()} - ${evento.tipoAtaque.replace(/_/g, ' ')}`,
|
||||
titulo: `🚨 ${evento.severidade.toUpperCase()} - ${tipoAtaqueLabel}`,
|
||||
descricao: evento.descricao,
|
||||
lida: false,
|
||||
criadaEm: Date.now()
|
||||
@@ -1557,6 +1752,183 @@ export const dispararAlertasInternos = internalMutation({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Função interna para enviar mensagem de chat do sistema
|
||||
*/
|
||||
export const enviarMensagemChatSistema = internalMutation({
|
||||
args: {
|
||||
usuarioSistemaId: v.id('usuarios'),
|
||||
usuarioDestinatarioId: v.id('usuarios'),
|
||||
mensagem: v.string()
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
// Buscar ou criar conversa individual entre sistema e destinatário
|
||||
const conversasExistentes = await ctx.db
|
||||
.query('conversas')
|
||||
.filter((q) => q.eq(q.field('tipo'), 'individual'))
|
||||
.collect();
|
||||
|
||||
let conversaId: Id<'conversas'> | null = null;
|
||||
|
||||
for (const conversa of conversasExistentes) {
|
||||
if (
|
||||
conversa.participantes.length === 2 &&
|
||||
conversa.participantes.includes(args.usuarioSistemaId) &&
|
||||
conversa.participantes.includes(args.usuarioDestinatarioId)
|
||||
) {
|
||||
conversaId = conversa._id;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!conversaId) {
|
||||
// Criar nova conversa
|
||||
conversaId = await ctx.db.insert('conversas', {
|
||||
tipo: 'individual',
|
||||
participantes: [args.usuarioSistemaId, args.usuarioDestinatarioId],
|
||||
criadoPor: args.usuarioSistemaId,
|
||||
criadoEm: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
// Criar mensagem
|
||||
const mensagemId = await ctx.db.insert('mensagens', {
|
||||
conversaId,
|
||||
remetenteId: args.usuarioSistemaId,
|
||||
conteudo: args.mensagem,
|
||||
conteudoBusca: args.mensagem.toLowerCase(),
|
||||
tipo: 'texto',
|
||||
enviadaEm: Date.now()
|
||||
});
|
||||
|
||||
// Atualizar última mensagem da conversa
|
||||
await ctx.db.patch(conversaId, {
|
||||
ultimaMensagem: args.mensagem.substring(0, 100),
|
||||
ultimaMensagemTimestamp: Date.now(),
|
||||
ultimaMensagemRemetenteId: args.usuarioSistemaId
|
||||
});
|
||||
|
||||
// Criar notificação para destinatário
|
||||
await ctx.db.insert('notificacoes', {
|
||||
usuarioId: args.usuarioDestinatarioId,
|
||||
tipo: 'nova_mensagem',
|
||||
conversaId,
|
||||
mensagemId,
|
||||
remetenteId: args.usuarioSistemaId,
|
||||
titulo: '🚨 Alerta de Segurança',
|
||||
descricao: args.mensagem.substring(0, 100),
|
||||
lida: false,
|
||||
criadaEm: Date.now()
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Notificar quando rate limit é excedido
|
||||
*/
|
||||
export const notificarRateLimitExcedido = internalMutation({
|
||||
args: {
|
||||
configId: v.id('rateLimitConfig'),
|
||||
tipo: v.union(
|
||||
v.literal('ip'),
|
||||
v.literal('usuario'),
|
||||
v.literal('endpoint'),
|
||||
v.literal('global')
|
||||
),
|
||||
identificador: v.string(),
|
||||
endpoint: v.string(),
|
||||
acaoExcedido: v.union(v.literal('bloquear'), v.literal('throttle'), v.literal('alertar')),
|
||||
limite: v.number(),
|
||||
janelaSegundos: v.number()
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const config = await ctx.db.get(args.configId);
|
||||
if (!config) return null;
|
||||
|
||||
// Buscar usuários TI para notificar
|
||||
const rolesTi = await ctx.db
|
||||
.query('roles')
|
||||
.filter((q) => q.eq(q.field('admin'), true))
|
||||
.collect();
|
||||
|
||||
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();
|
||||
for (const usuario of membros) {
|
||||
usuariosNotificados.push(usuario._id);
|
||||
}
|
||||
}
|
||||
|
||||
// Criar notificações para usuários TI
|
||||
const tipoAcao =
|
||||
args.acaoExcedido === 'bloquear'
|
||||
? 'bloqueado'
|
||||
: args.acaoExcedido === 'alertar'
|
||||
? 'alertado'
|
||||
: 'throttled';
|
||||
const emoji = args.acaoExcedido === 'bloquear' ? '🚫' : '⚠️';
|
||||
const titulo = `${emoji} Rate Limit ${tipoAcao === 'bloqueado' ? 'Bloqueado' : tipoAcao === 'alertado' ? 'Alertado' : 'Throttled'}`;
|
||||
const descricao = `${args.tipo.toUpperCase()}: ${args.identificador} excedeu o limite de ${args.limite} requisições em ${args.janelaSegundos}s no endpoint ${args.endpoint}`;
|
||||
|
||||
for (const usuarioId of usuariosNotificados) {
|
||||
await ctx.db.insert('notificacoes', {
|
||||
usuarioId,
|
||||
tipo: 'alerta_seguranca',
|
||||
conversaId: undefined,
|
||||
mensagemId: undefined,
|
||||
remetenteId: undefined,
|
||||
titulo,
|
||||
descricao,
|
||||
lida: false,
|
||||
criadaEm: Date.now()
|
||||
});
|
||||
}
|
||||
|
||||
// Criar evento de segurança se foi bloqueado
|
||||
if (args.acaoExcedido === 'bloquear') {
|
||||
// Determinar tipo de ataque baseado no contexto
|
||||
let tipoAtaque: AtaqueCiberneticoTipo = 'brute_force';
|
||||
if (args.tipo === 'ip') {
|
||||
tipoAtaque = 'ddos';
|
||||
} else if (args.tipo === 'usuario') {
|
||||
tipoAtaque = 'brute_force';
|
||||
}
|
||||
|
||||
// Criar evento de segurança
|
||||
const eventoId = await ctx.db.insert('securityEvents', {
|
||||
referencia: `rate_limit_${args.tipo}_${args.identificador}_${Date.now()}`,
|
||||
timestamp: Date.now(),
|
||||
tipoAtaque,
|
||||
severidade: 'alto',
|
||||
status: 'detectado',
|
||||
descricao: `Rate limit bloqueado: ${args.identificador} excedeu ${args.limite} requisições em ${args.janelaSegundos}s`,
|
||||
origemIp: args.tipo === 'ip' ? args.identificador : undefined,
|
||||
tags: ['rate_limit', 'bloqueio_automatico'],
|
||||
atualizadoEm: Date.now()
|
||||
});
|
||||
|
||||
// Disparar alertas se configurado
|
||||
ctx.scheduler
|
||||
.runAfter(0, internal.security.dispararAlertasInternos, {
|
||||
eventoId
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erro ao agendar alertas de rate limit:', error);
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
export const expirarBloqueiosIpAutomaticos = internalMutation({
|
||||
args: {},
|
||||
returns: v.null(),
|
||||
@@ -1692,6 +2064,24 @@ async function aplicarRateLimit(
|
||||
if (!result.ok) {
|
||||
const retryAfter = result.retryAfter ?? periodo;
|
||||
|
||||
// Criar notificações e eventos quando rate limit é excedido
|
||||
// Usar scheduler para não bloquear a requisição
|
||||
if ('runMutation' in ctx) {
|
||||
ctx.scheduler
|
||||
.runAfter(0, internal.security.notificarRateLimitExcedido, {
|
||||
configId: config._id,
|
||||
tipo,
|
||||
identificador,
|
||||
endpoint: endpoint ?? 'default',
|
||||
acaoExcedido: config.acaoExcedido,
|
||||
limite: config.limite,
|
||||
janelaSegundos: config.janelaSegundos
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Erro ao agendar notificação de rate limit:', error);
|
||||
});
|
||||
}
|
||||
|
||||
if (config.acaoExcedido === 'bloquear') {
|
||||
return {
|
||||
permitido: false,
|
||||
|
||||
Reference in New Issue
Block a user