From f3b4721119a67000b83bf847aaa2e59a242123fc Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Sat, 6 Dec 2025 19:34:00 -0300 Subject: [PATCH] feat: add templateCodigo field to alert configurations and enhance alert handling with new email/chat templates for cybersecurity incidents --- .../components/ti/CybersecurityWizcard.svelte | 4 +- packages/backend/convex/security.ts | 277 +++++++++++++++++- packages/backend/convex/tables/system.ts | 1 + packages/backend/convex/templatesMensagens.ts | 158 ++++++++++ 4 files changed, 437 insertions(+), 3 deletions(-) diff --git a/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte b/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte index 97f0d08..560ed71 100644 --- a/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte +++ b/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte @@ -122,6 +122,7 @@ severidadeMin: alertSeveridadeMin, tiposAtaque: alertTiposAtaque as AtaqueCiberneticoTipo[], reenvioMin: alertReenvioMin, + templateCodigo: alertTemplate, // Incluir template selecionado criadoPor: obterUsuarioId() }); feedback = { @@ -152,9 +153,10 @@ severidadeMin: SeveridadeSeguranca; tiposAtaque?: AtaqueCiberneticoTipo[]; reenvioMin: number; + templateCodigo?: string; }) { alertNomeConfig = config.nome ?? ''; - alertTemplate = 'incidente_critico'; // Mantém o template padrão + alertTemplate = config.templateCodigo ?? 'incidente_critico'; // Usar template salvo ou padrão enviarPorEmail = config.canais?.email ?? true; enviarPorChat = config.canais?.chat ?? false; alertEmails = (config.emails ?? []).join('\n'); diff --git a/packages/backend/convex/security.ts b/packages/backend/convex/security.ts index c78f30d..477558f 100644 --- a/packages/backend/convex/security.ts +++ b/packages/backend/convex/security.ts @@ -1,6 +1,6 @@ import { v } from 'convex/values'; import { internalMutation, mutation, query } from './_generated/server'; -import { internal } from './_generated/api'; +import { internal, api } from './_generated/api'; import type { Id } from './_generated/dataModel'; import type { AtaqueCiberneticoTipo, @@ -1436,6 +1436,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() }) @@ -1456,6 +1457,7 @@ export const listarAlertConfigs = query({ severidadeMin: r.severidadeMin, tiposAtaque: r.tiposAtaque, reenvioMin: r.reenvioMin, + templateCodigo: r.templateCodigo, criadoEm: r.criadoEm, atualizadoEm: r.atualizadoEm })); @@ -1472,6 +1474,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') }), @@ -1486,6 +1489,7 @@ export const salvarAlertConfig = mutation({ severidadeMin: args.severidadeMin, tiposAtaque: args.tiposAtaque, reenvioMin: args.reenvioMin, + templateCodigo: args.templateCodigo, atualizadoEm: agora }); return { _id: args.configId }; @@ -1498,6 +1502,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 @@ -1523,6 +1528,200 @@ 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.FRONTEND_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 = { + informativo: 'Informativo', + baixo: 'Baixo', + moderado: 'Moderado', + alto: 'Alto', + critico: 'Crítico' + }; + + // Mapear tipo de ataque para texto legível + const attackLabels: Record = { + 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') + .withIndex('by_nivel', (q) => q.lte('nivel', 1)) + .first(); + let usuarioSistema: Id<'usuarios'> | undefined; + if (rolesTi) { + const usuarioTi = await ctx.db + .query('usuarios') + .withIndex('by_role', (q) => q.eq('roleId', rolesTi._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') .withIndex('by_nivel', (q) => q.lte('nivel', 1)) @@ -1547,7 +1746,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() @@ -1558,6 +1757,80 @@ 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', + criadaEm: 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; + } +}); + export const expirarBloqueiosIpAutomaticos = internalMutation({ args: {}, returns: v.null(), diff --git a/packages/backend/convex/tables/system.ts b/packages/backend/convex/tables/system.ts index 395c3c8..6cb4294 100644 --- a/packages/backend/convex/tables/system.ts +++ b/packages/backend/convex/tables/system.ts @@ -164,6 +164,7 @@ export const systemTables = { severidadeMin: severidadeSeguranca, tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)), reenvioMin: v.number(), + templateCodigo: v.optional(v.string()), // Template a ser usado para email/chat criadoPor: v.id('usuarios'), criadoEm: v.number(), atualizadoEm: v.number() diff --git a/packages/backend/convex/templatesMensagens.ts b/packages/backend/convex/templatesMensagens.ts index 7629e48..b1909de 100644 --- a/packages/backend/convex/templatesMensagens.ts +++ b/packages/backend/convex/templatesMensagens.ts @@ -701,6 +701,164 @@ export const criarTemplatesPadrao = mutation({ ], categoria: 'email' as const, tags: ['ausencia', 'reprovacao', 'gestao'] + }, + // ===================== ALERTAS DE SEGURANÇA CIBERNÉTICA ===================== + { + codigo: 'incidente_critico', + nome: 'Incidente Crítico - Ação Imediata', + titulo: '🚨 ALERTA CRÍTICO: {{tipoAtaque}}', + corpo: + "" + + "
" + + "

🚨 ALERTA CRÍTICO DE SEGURANÇA

" + + '

Olá {{destinatarioNome}},

' + + '

Um incidente crítico de segurança foi detectado no sistema:

' + + "
" + + "

Tipo de Ataque: {{tipoAtaque}}

" + + "

Severidade: {{severidade}}

" + + "

Descrição: {{descricao}}

" + + "

IP de Origem: {{origemIp}}

" + + "

Data/Hora: {{dataHora}}

" + + '
' + + "

⚠️ AÇÃO IMEDIATA NECESSÁRIA

" + + "

" + + "" + + 'Ver Detalhes do Incidente' + + '' + + '

' + + "

" + + 'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de Segurança' + + '

' + + '
', + variaveis: [ + 'destinatarioNome', + 'tipoAtaque', + 'severidade', + 'descricao', + 'origemIp', + 'dataHora', + 'urlSistema' + ], + categoria: 'email' as const, + tags: ['seguranca', 'alerta', 'critico', 'cybersecurity'] + }, + { + codigo: 'bloqueio_automatico', + nome: 'Bloqueio Automático', + titulo: '🔒 Bloqueio Automático: {{tipoAtaque}}', + corpo: + "" + + "
" + + "

🔒 Bloqueio Automático Aplicado

" + + '

Olá {{destinatarioNome}},

' + + '

O sistema aplicou um bloqueio automático devido a uma tentativa de ataque detectada:

' + + "
" + + "

Tipo de Ataque: {{tipoAtaque}}

" + + "

IP Bloqueado: {{origemIp}}

" + + "

Descrição: {{descricao}}

" + + "

Data/Hora: {{dataHora}}

" + + '
' + + "

" + + "" + + 'Ver Detalhes do Bloqueio' + + '' + + '

' + + "

" + + 'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de Segurança' + + '

' + + '
', + variaveis: [ + 'destinatarioNome', + 'tipoAtaque', + 'origemIp', + 'descricao', + 'dataHora', + 'urlSistema' + ], + categoria: 'email' as const, + tags: ['seguranca', 'bloqueio', 'automatico', 'cybersecurity'] + }, + { + codigo: 'sumario_30min', + nome: 'Sumário 30 Min', + titulo: '📊 Sumário de Segurança - Últimos 30 minutos', + corpo: + "" + + "
" + + "

📊 Sumário de Segurança

" + + '

Olá {{destinatarioNome}},

' + + '

Resumo dos eventos de segurança dos últimos 30 minutos:

' + + "
" + + "

Total de Eventos: {{totalEventos}}

" + + "

Eventos Críticos: {{eventosCriticos}}

" + + "

Eventos Altos: {{eventosAltos}}

" + + "

IPs Bloqueados: {{ipsBloqueados}}

" + + '
' + + "

" + + "" + + 'Ver Relatório Completo' + + '' + + '

' + + "

" + + 'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de Segurança' + + '

' + + '
', + variaveis: [ + 'destinatarioNome', + 'totalEventos', + 'eventosCriticos', + 'eventosAltos', + 'ipsBloqueados', + 'urlSistema' + ], + categoria: 'email' as const, + tags: ['seguranca', 'sumario', 'relatorio', 'cybersecurity'] + }, + { + codigo: 'anormalidade', + nome: 'Anomalia Detectada', + titulo: '⚠️ Anomalia Detectada: {{tipoAtaque}}', + corpo: + "" + + "
" + + "

⚠️ Anomalia Detectada

" + + '

Olá {{destinatarioNome}},

' + + '

O sistema detectou uma anomalia de segurança que requer atenção:

' + + "
" + + "

Tipo de Ataque: {{tipoAtaque}}

" + + "

Severidade: {{severidade}}

" + + "

Descrição: {{descricao}}

" + + "

IP de Origem: {{origemIp}}

" + + "

Data/Hora: {{dataHora}}

" + + '
' + + "

" + + "" + + 'Ver Detalhes da Anomalia' + + '' + + '

' + + "

" + + 'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de Segurança' + + '

' + + '
', + variaveis: [ + 'destinatarioNome', + 'tipoAtaque', + 'severidade', + 'descricao', + 'origemIp', + 'dataHora', + 'urlSistema' + ], + categoria: 'email' as const, + tags: ['seguranca', 'anomalia', 'alerta', 'cybersecurity'] } ];