feat: add templateCodigo field to alert configurations and enhance alert handling with new email/chat templates for cybersecurity incidents

This commit is contained in:
2025-12-06 19:34:00 -03:00
parent 1ceef73847
commit f3b4721119
4 changed files with 437 additions and 3 deletions

View File

@@ -122,6 +122,7 @@
severidadeMin: alertSeveridadeMin, severidadeMin: alertSeveridadeMin,
tiposAtaque: alertTiposAtaque as AtaqueCiberneticoTipo[], tiposAtaque: alertTiposAtaque as AtaqueCiberneticoTipo[],
reenvioMin: alertReenvioMin, reenvioMin: alertReenvioMin,
templateCodigo: alertTemplate, // Incluir template selecionado
criadoPor: obterUsuarioId() criadoPor: obterUsuarioId()
}); });
feedback = { feedback = {
@@ -152,9 +153,10 @@
severidadeMin: SeveridadeSeguranca; severidadeMin: SeveridadeSeguranca;
tiposAtaque?: AtaqueCiberneticoTipo[]; tiposAtaque?: AtaqueCiberneticoTipo[];
reenvioMin: number; reenvioMin: number;
templateCodigo?: string;
}) { }) {
alertNomeConfig = config.nome ?? ''; 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; enviarPorEmail = config.canais?.email ?? true;
enviarPorChat = config.canais?.chat ?? false; enviarPorChat = config.canais?.chat ?? false;
alertEmails = (config.emails ?? []).join('\n'); alertEmails = (config.emails ?? []).join('\n');

View File

@@ -1,6 +1,6 @@
import { v } from 'convex/values'; 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 { internal, api } from './_generated/api';
import type { Id } from './_generated/dataModel'; import type { Id } from './_generated/dataModel';
import type { import type {
AtaqueCiberneticoTipo, AtaqueCiberneticoTipo,
@@ -1436,6 +1436,7 @@ export const listarAlertConfigs = query({
severidadeMin: severidadeValidator, severidadeMin: severidadeValidator,
tiposAtaque: v.optional(v.array(ataqueValidator)), tiposAtaque: v.optional(v.array(ataqueValidator)),
reenvioMin: v.number(), reenvioMin: v.number(),
templateCodigo: v.optional(v.string()),
criadoEm: v.number(), criadoEm: v.number(),
atualizadoEm: v.number() atualizadoEm: v.number()
}) })
@@ -1456,6 +1457,7 @@ export const listarAlertConfigs = query({
severidadeMin: r.severidadeMin, severidadeMin: r.severidadeMin,
tiposAtaque: r.tiposAtaque, tiposAtaque: r.tiposAtaque,
reenvioMin: r.reenvioMin, reenvioMin: r.reenvioMin,
templateCodigo: r.templateCodigo,
criadoEm: r.criadoEm, criadoEm: r.criadoEm,
atualizadoEm: r.atualizadoEm atualizadoEm: r.atualizadoEm
})); }));
@@ -1472,6 +1474,7 @@ export const salvarAlertConfig = mutation({
severidadeMin: severidadeValidator, severidadeMin: severidadeValidator,
tiposAtaque: v.optional(v.array(ataqueValidator)), tiposAtaque: v.optional(v.array(ataqueValidator)),
reenvioMin: v.number(), reenvioMin: v.number(),
templateCodigo: v.optional(v.string()), // Template a ser usado
criadoPor: v.id('usuarios') criadoPor: v.id('usuarios')
}, },
returns: v.object({ _id: v.id('alertConfigs') }), returns: v.object({ _id: v.id('alertConfigs') }),
@@ -1486,6 +1489,7 @@ export const salvarAlertConfig = mutation({
severidadeMin: args.severidadeMin, severidadeMin: args.severidadeMin,
tiposAtaque: args.tiposAtaque, tiposAtaque: args.tiposAtaque,
reenvioMin: args.reenvioMin, reenvioMin: args.reenvioMin,
templateCodigo: args.templateCodigo,
atualizadoEm: agora atualizadoEm: agora
}); });
return { _id: args.configId }; return { _id: args.configId };
@@ -1498,6 +1502,7 @@ export const salvarAlertConfig = mutation({
severidadeMin: args.severidadeMin, severidadeMin: args.severidadeMin,
tiposAtaque: args.tiposAtaque, tiposAtaque: args.tiposAtaque,
reenvioMin: args.reenvioMin, reenvioMin: args.reenvioMin,
templateCodigo: args.templateCodigo ?? 'incidente_critico', // Padrão
criadoPor: args.criadoPor, criadoPor: args.criadoPor,
criadoEm: agora, criadoEm: agora,
atualizadoEm: agora atualizadoEm: agora
@@ -1523,6 +1528,200 @@ export const dispararAlertasInternos = internalMutation({
const evento = await ctx.db.get(args.eventoId); const evento = await ctx.db.get(args.eventoId);
if (!evento) return null; 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<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')
.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 const rolesTi = await ctx.db
.query('roles') .query('roles')
.withIndex('by_nivel', (q) => q.lte('nivel', 1)) .withIndex('by_nivel', (q) => q.lte('nivel', 1))
@@ -1547,7 +1746,7 @@ export const dispararAlertasInternos = internalMutation({
conversaId: undefined, conversaId: undefined,
mensagemId: undefined, mensagemId: undefined,
remetenteId: undefined, remetenteId: undefined,
titulo: `🚨 ${evento.severidade.toUpperCase()} - ${evento.tipoAtaque.replace(/_/g, ' ')}`, titulo: `🚨 ${evento.severidade.toUpperCase()} - ${tipoAtaqueLabel}`,
descricao: evento.descricao, descricao: evento.descricao,
lida: false, lida: false,
criadaEm: Date.now() 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({ export const expirarBloqueiosIpAutomaticos = internalMutation({
args: {}, args: {},
returns: v.null(), returns: v.null(),

View File

@@ -164,6 +164,7 @@ export const systemTables = {
severidadeMin: severidadeSeguranca, severidadeMin: severidadeSeguranca,
tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)), tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)),
reenvioMin: v.number(), reenvioMin: v.number(),
templateCodigo: v.optional(v.string()), // Template a ser usado para email/chat
criadoPor: v.id('usuarios'), criadoPor: v.id('usuarios'),
criadoEm: v.number(), criadoEm: v.number(),
atualizadoEm: v.number() atualizadoEm: v.number()

View File

@@ -701,6 +701,164 @@ export const criarTemplatesPadrao = mutation({
], ],
categoria: 'email' as const, categoria: 'email' as const,
tags: ['ausencia', 'reprovacao', 'gestao'] 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:
"<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>" +
"<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>" +
"<h2 style='color: #DC2626;'>🚨 ALERTA CRÍTICO DE SEGURANÇA</h2>" +
'<p>Olá <strong>{{destinatarioNome}}</strong>,</p>' +
'<p>Um <strong>incidente crítico de segurança</strong> foi detectado no sistema:</p>' +
"<div style='background-color: #FEF2F2; border-left: 4px solid #DC2626; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
"<p style='margin: 0;'><strong>Tipo de Ataque:</strong> {{tipoAtaque}}</p>" +
"<p style='margin: 5px 0 0 0;'><strong>Severidade:</strong> <span style='color: #DC2626; font-weight: bold;'>{{severidade}}</span></p>" +
"<p style='margin: 5px 0 0 0;'><strong>Descrição:</strong> {{descricao}}</p>" +
"<p style='margin: 5px 0 0 0;'><strong>IP de Origem:</strong> {{origemIp}}</p>" +
"<p style='margin: 5px 0 0 0;'><strong>Data/Hora:</strong> {{dataHora}}</p>" +
'</div>' +
"<p style='color: #DC2626; font-weight: bold;'>⚠️ AÇÃO IMEDIATA NECESSÁRIA</p>" +
"<p style='margin-top: 30px;'>" +
"<a href='{{urlSistema}}/ti/cybersecurity' " +
"style='background-color: #DC2626; color: white; padding: 12px 24px; " +
"text-decoration: none; border-radius: 6px; display: inline-block;'>" +
'Ver Detalhes do Incidente' +
'</a>' +
'</p>' +
"<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>" +
'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de Segurança' +
'</p>' +
'</div></body></html>',
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:
"<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>" +
"<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>" +
"<h2 style='color: #F59E0B;'>🔒 Bloqueio Automático Aplicado</h2>" +
'<p>Olá <strong>{{destinatarioNome}}</strong>,</p>' +
'<p>O sistema aplicou um <strong>bloqueio automático</strong> devido a uma tentativa de ataque detectada:</p>' +
"<div style='background-color: #FFFBEB; border-left: 4px solid #F59E0B; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
"<p style='margin: 0;'><strong>Tipo de Ataque:</strong> {{tipoAtaque}}</p>" +
"<p style='margin: 5px 0 0 0;'><strong>IP Bloqueado:</strong> {{origemIp}}</p>" +
"<p style='margin: 5px 0 0 0;'><strong>Descrição:</strong> {{descricao}}</p>" +
"<p style='margin: 5px 0 0 0;'><strong>Data/Hora:</strong> {{dataHora}}</p>" +
'</div>' +
"<p style='margin-top: 30px;'>" +
"<a href='{{urlSistema}}/ti/cybersecurity' " +
"style='background-color: #F59E0B; color: white; padding: 12px 24px; " +
"text-decoration: none; border-radius: 6px; display: inline-block;'>" +
'Ver Detalhes do Bloqueio' +
'</a>' +
'</p>' +
"<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>" +
'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de Segurança' +
'</p>' +
'</div></body></html>',
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:
"<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>" +
"<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>" +
"<h2 style='color: #2563EB;'>📊 Sumário de Segurança</h2>" +
'<p>Olá <strong>{{destinatarioNome}}</strong>,</p>' +
'<p>Resumo dos eventos de segurança dos últimos 30 minutos:</p>' +
"<div style='background-color: #EFF6FF; border-left: 4px solid #2563EB; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
"<p style='margin: 0;'><strong>Total de Eventos:</strong> {{totalEventos}}</p>" +
"<p style='margin: 5px 0 0 0;'><strong>Eventos Críticos:</strong> {{eventosCriticos}}</p>" +
"<p style='margin: 5px 0 0 0;'><strong>Eventos Altos:</strong> {{eventosAltos}}</p>" +
"<p style='margin: 5px 0 0 0;'><strong>IPs Bloqueados:</strong> {{ipsBloqueados}}</p>" +
'</div>' +
"<p style='margin-top: 30px;'>" +
"<a href='{{urlSistema}}/ti/cybersecurity' " +
"style='background-color: #2563EB; color: white; padding: 12px 24px; " +
"text-decoration: none; border-radius: 6px; display: inline-block;'>" +
'Ver Relatório Completo' +
'</a>' +
'</p>' +
"<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>" +
'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de Segurança' +
'</p>' +
'</div></body></html>',
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:
"<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>" +
"<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>" +
"<h2 style='color: #F59E0B;'>⚠️ Anomalia Detectada</h2>" +
'<p>Olá <strong>{{destinatarioNome}}</strong>,</p>' +
'<p>O sistema detectou uma <strong>anomalia de segurança</strong> que requer atenção:</p>' +
"<div style='background-color: #FFFBEB; border-left: 4px solid #F59E0B; padding: 15px; border-radius: 8px; margin: 20px 0;'>" +
"<p style='margin: 0;'><strong>Tipo de Ataque:</strong> {{tipoAtaque}}</p>" +
"<p style='margin: 5px 0 0 0;'><strong>Severidade:</strong> {{severidade}}</p>" +
"<p style='margin: 5px 0 0 0;'><strong>Descrição:</strong> {{descricao}}</p>" +
"<p style='margin: 5px 0 0 0;'><strong>IP de Origem:</strong> {{origemIp}}</p>" +
"<p style='margin: 5px 0 0 0;'><strong>Data/Hora:</strong> {{dataHora}}</p>" +
'</div>' +
"<p style='margin-top: 30px;'>" +
"<a href='{{urlSistema}}/ti/cybersecurity' " +
"style='background-color: #F59E0B; color: white; padding: 12px 24px; " +
"text-decoration: none; border-radius: 6px; display: inline-block;'>" +
'Ver Detalhes da Anomalia' +
'</a>' +
'</p>' +
"<p style='color: #6B7280; font-size: 12px; margin-top: 30px;'>" +
'SGSE - Sistema de Gerenciamento de Secretaria - Equipe de Segurança' +
'</p>' +
'</div></body></html>',
variaveis: [
'destinatarioNome',
'tipoAtaque',
'severidade',
'descricao',
'origemIp',
'dataHora',
'urlSistema'
],
categoria: 'email' as const,
tags: ['seguranca', 'anomalia', 'alerta', 'cybersecurity']
} }
]; ];