import { RateLimiter, SECOND } from '@convex-dev/rate-limiter'; import { v } from 'convex/values'; import { internalMutation, mutation, MutationCtx, query, QueryCtx } from './_generated/server'; import { internal, api, components } from './_generated/api'; import type { Id } from './_generated/dataModel'; import { getCurrentUserFunction } from './auth'; import type { AtaqueCiberneticoTipo, SeveridadeSeguranca, StatusEventoSeguranca } from './tables/security'; type Indicador = { tipo: string; valor: string; confianca?: number; }; type RegistroMetricas = { pps?: number; bps?: number; rpm?: number; errosPorSegundo?: number; hostsAfetados?: number; }; type RegistroFingerprint = { userAgent?: string; deviceId?: string; ja3?: string; tlsVersion?: string; }; type RegistroGeo = { pais?: string; regiao?: string; cidade?: string; latitude?: number; longitude?: number; }; async function assertAdmin(ctx: QueryCtx | MutationCtx) { const usuarioAtual = await getCurrentUserFunction(ctx); if (!usuarioAtual) { throw new Error('acesso_negado'); } if (!usuarioAtual.roleId) { throw new Error('acesso_negado'); } const role = await ctx.db.get(usuarioAtual.roleId); if (!role || role.admin !== true) { throw new Error('acesso_negado'); } return usuarioAtual; } function assertDevOnly() { // Em produção, endpoints/mutations de teste devem ser desabilitados. // Observação: em ambientes Convex, NODE_ENV pode vir como 'production' mesmo em dev local, // então também aceitamos deployments locais "anonymous-*", ou uma flag explícita. const deployment = process.env.CONVEX_DEPLOYMENT; const siteUrl = process.env.SITE_URL || process.env.CONVEX_SITE_URL; const enabled = process.env.SECURITY_DEV_TOOLS === 'true' || (typeof siteUrl === 'string' && /localhost|127\.0\.0\.1/i.test(siteUrl)) || (typeof deployment === 'string' && deployment.startsWith('anonymous-')) || process.env.NODE_ENV !== 'production'; if (!enabled) throw new Error('acesso_negado'); } type RegistroEventoArgs = { descricao?: string; tipoAtaque?: AtaqueCiberneticoTipo; severidade?: SeveridadeSeguranca; origemIp?: string; destinoIp?: string; destinoPorta?: number; protocolo?: string; transporte?: string; origemRegiao?: string; origemAsn?: string; mitreTechnique?: string; indicadores?: Indicador[]; metricas?: RegistroMetricas; fingerprint?: RegistroFingerprint; geolocalizacao?: RegistroGeo; tags?: string[]; }; const ATAQUES_PRIORITARIOS: Array = [ 'ddos', 'ransomware', 'ransomware_lateral', 'apt', 'zero_day', 'botnet', 'ot_ics', 'quantum_attack', 'polymorphic_malware', 'fileless_malware', 'firmware_bootloader', 'adversarial_ai', 'deepfake_phishing', 'phishing', 'sql_injection', 'xss', 'path_traversal', 'command_injection', 'nosql_injection', 'xxe', 'man_in_the_middle', 'credential_stuffing', 'brute_force', 'supply_chain', 'malware', 'cve_exploit', 'bec', 'side_channel' ]; const BASE_SEVERIDADE: Record = { phishing: 'moderado', malware: 'moderado', ransomware: 'alto', brute_force: 'moderado', credential_stuffing: 'moderado', sql_injection: 'alto', xss: 'moderado', path_traversal: 'alto', command_injection: 'alto', nosql_injection: 'alto', xxe: 'alto', man_in_the_middle: 'alto', ddos: 'alto', cve_exploit: 'alto', apt: 'critico', zero_day: 'critico', supply_chain: 'critico', fileless_malware: 'alto', polymorphic_malware: 'alto', ransomware_lateral: 'critico', deepfake_phishing: 'alto', adversarial_ai: 'alto', side_channel: 'alto', firmware_bootloader: 'critico', bec: 'alto', botnet: 'alto', ot_ics: 'critico', quantum_attack: 'critico' }; const SEVERIDADE_SCORE: Record = { informativo: 0, baixo: 1, moderado: 2, alto: 3, critico: 4 }; const SCORE_SEVERIDADE = Object.entries(SEVERIDADE_SCORE).reduce< Record >((acc, [nome, score]) => { acc[score] = nome as SeveridadeSeguranca; return acc; }, {}); // Padrões robustos de detecção de ataques const KEYWORDS: Record = { phishing: [/phish/i, /spoof/i, /fake login/i, /phishing/i], malware: [/malware/i, /payload/i, /trojan/i, /virus/i, /worm/i], ransomware: [/ransom/i, /encrypt/i, /locker/i, /cryptolocker/i], brute_force: [ /brute/i, /password guess/i, /login flood/i, /multiple failed login/i, /repeated login attempt/i ], credential_stuffing: [/credential/i, /stuffing/i, /combo/i, /credential dump/i], // SQL Injection - padrões mais robustos sql_injection: [ /select\s+.*from/i, /union\s+select/i, /union\s+all\s+select/i, /insert\s+into/i, /delete\s+from/i, /update\s+.*set/i, /drop\s+table/i, /exec\s*\(/i, /execute\s*\(/i, /';?\s*(or|and)\s+['"]?\d+['"]?\s*=\s*['"]?\d+/i, /';?\s*(or|and)\s+['"]?1['"]?\s*=\s*['"]?1/i, /';?\s*(or|and)\s+['"]?1['"]?\s*=\s*['"]?2/i, /\bor\b\s*1\s*=\s*1\b/i, /\band\b\s*1\s*=\s*1\b/i, /(['"])\s*or\s*\1?\s*1\s*=\s*1/i, /\)\s*or\s*\(\s*'1'\s*=\s*'1'\s*\)/i, /';?\s*--/i, /';?\s*\/\*/i, /\/\*.*\*\//i, /benchmark\s*\(/i, /sleep\s*\(/i, /waitfor\s+delay/i, /pg_sleep\s*\(/i, /load_file\s*\(/i, /into\s+outfile/i, /into\s+dumpfile/i, /char\s*\(/i, /ascii\s*\(/i, /hex\s*\(/i, /convert\s*\(/i, /cast\s*\(/i ], // XSS - padrões mais robustos xss: [ /]*>/i, /<\/script>/i, /javascript:/i, /onerror\s*=/i, /onload\s*=/i, /onclick\s*=/i, /onmouseover\s*=/i, /onfocus\s*=/i, /onblur\s*=/i, /onchange\s*=/i, /onsubmit\s*=/i, /]*src\s*=\s*[^>]*javascript:/i, /]*>/i, /]*>/i, /]*>/i, /alert\s*\(/i, /prompt\s*\(/i, /confirm\s*\(/i, /document\.cookie/i, /document\.write/i, /eval\s*\(/i, /expression\s*\(/i, /vbscript:/i, /data:text\/html/i ], // Path Traversal path_traversal: [ /\.\.\/\.\.\//i, /\.\.\\\.\.\\/i, /\.\.%2f/i, /\.\.%5c/i, /%2e%2e%2f/i, /%2e%2e%5c/i, /\.\.%252f/i, /\.\.%255c/i, /\.\.\/\.\.\/\.\.\//i, /etc\/passwd/i, /proc\/self\/environ/i, /windows\/win\.ini/i, /\.\.\/\.\.\/\.\.\/\.\.\//i ], // Command Injection command_injection: [ /;\s*(ls|cat|pwd|whoami|id|uname|ps|netstat)/i, /\|\s*(ls|cat|pwd|whoami|id|uname|ps|netstat)/i, /&&\s*(ls|cat|pwd|whoami|id|uname|ps|netstat)/i, /\|\|\s*(ls|cat|pwd|whoami|id|uname|ps|netstat)/i, /`[^`]*(ls|cat|pwd|whoami|id|uname|ps|netstat)[^`]*`/i, /\$\([^)]*(ls|cat|pwd|whoami|id|uname|ps|netstat)[^)]*\)/i, /exec\s*\(/i, /system\s*\(/i, /passthru\s*\(/i, /shell_exec\s*\(/i, /proc_open\s*\(/i, /popen\s*\(/i ], // NoSQL Injection nosql_injection: [ /\$where/i, /\$ne/i, /\$gt/i, /\$lt/i, /\$gte/i, /\$lte/i, /\$in/i, /\$nin/i, /\$regex/i, /\$exists/i, /\$or/i, /\$and/i, /\$nor/i, /\$not/i, /\$elemMatch/i, /\$size/i, /\$type/i, /\$mod/i, /\$text/i, /\$geoWithin/i, /\$geoIntersects/i, /\$near/i, /\$nearSphere/i, /\$all/i, /\$slice/i, /\$comment/i, /\$explain/i, /\$hint/i, /\$maxScan/i, /\$maxTimeMS/i, /\$min/i, /\$max/i, /\$orderby/i, /\$query/i, /\$returnKey/i, /\$showDiskLoc/i, /\$natural/i ], // XXE (XML External Entity) xxe: [ /]*\[/i, / = new Set(['alto', 'critico']); const ataqueValidator = v.union( v.literal('phishing'), v.literal('malware'), v.literal('ransomware'), v.literal('brute_force'), v.literal('credential_stuffing'), v.literal('sql_injection'), 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('ddos'), v.literal('cve_exploit'), v.literal('apt'), v.literal('zero_day'), v.literal('supply_chain'), v.literal('fileless_malware'), v.literal('polymorphic_malware'), v.literal('ransomware_lateral'), v.literal('deepfake_phishing'), v.literal('adversarial_ai'), v.literal('side_channel'), v.literal('firmware_bootloader'), v.literal('bec'), v.literal('botnet'), v.literal('ot_ics'), v.literal('quantum_attack') ); const severidadeValidator = v.union( v.literal('informativo'), v.literal('baixo'), v.literal('moderado'), v.literal('alto'), v.literal('critico') ); const statusValidator = v.union( v.literal('detectado'), v.literal('investigando'), v.literal('contido'), v.literal('falso_positivo'), v.literal('escalado'), v.literal('resolvido') ); const indicadorCategoriaValidator = v.union( v.literal('ip'), v.literal('dominio'), v.literal('hash'), v.literal('email') ); const portActionValidator = v.union( v.literal('permitir'), v.literal('bloquear'), v.literal('monitorar'), v.literal('rate_limit') ); const protocoloValidator = v.union( v.literal('tcp'), v.literal('udp'), v.literal('icmp'), v.literal('quic'), v.literal('any') ); const acaoIncidenteValidator = v.union( v.literal('block_ip'), v.literal('unblock_ip'), v.literal('block_port'), v.literal('liberar_porta'), v.literal('notificar'), v.literal('isolar_host'), v.literal('gerar_relatorio'), v.literal('criar_ticket'), v.literal('ajuste_regra'), v.literal('custom') ); const acaoOrigemValidator = v.union(v.literal('automatico'), v.literal('manual')); export const enforceRequest = mutation({ args: { ip: v.string(), path: v.string(), method: v.string() }, returns: v.object({ allowed: v.boolean(), status: v.number(), reason: v.optional(v.string()), retryAfterMs: v.optional(v.number()) }), handler: async (ctx, args) => { const agora = Date.now(); const ip = args.ip.trim(); const path = args.path.trim() || '/'; const pathKey = path.replace(/^\/+/, ''); // 1) Blacklist enforcement (somente IP) const registroIp = await ctx.db .query('ipReputation') .withIndex('by_indicador', (q) => q.eq('indicador', ip)) .order('desc') .first(); if (registroIp && registroIp.categoria === 'ip' && registroIp.blacklist === true) { const ativo = !registroIp.bloqueadoAte || registroIp.bloqueadoAte > agora; if (ativo) { return { allowed: false, status: 403, reason: 'ip_blacklisted' }; } } // 2) Rate limit enforcement (endpoint + IP) // - endpoint rules usam o "pathKey" (ex.: api/auth/sign-in/email) // - ip rules usam o IP e namespace por endpoint const endpointCheck = await aplicarRateLimit(ctx, 'endpoint', pathKey || 'root', pathKey || 'root'); if (!endpointCheck.permitido) { return { allowed: false, status: 429, reason: endpointCheck.motivo ?? 'rate_limited', retryAfterMs: endpointCheck.retryAfter }; } const ipCheck = await aplicarRateLimit(ctx, 'ip', ip, pathKey || 'root'); if (!ipCheck.permitido) { return { allowed: false, status: 429, reason: ipCheck.motivo ?? 'rate_limited', retryAfterMs: ipCheck.retryAfter }; } // NOTE: para estratégias "throttle", o helper pode retornar retryAfter mas permitir. // Aqui optamos por permitir, deixando o servidor decidir se atrasa ou apenas sinaliza. const retryAfterMs = (endpointCheck.retryAfter ?? 0) > 0 ? endpointCheck.retryAfter : ipCheck.retryAfter; return { allowed: true, status: 200, reason: undefined, retryAfterMs: retryAfterMs && retryAfterMs > 0 ? retryAfterMs : undefined }; } }); // 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]; if (patterns && patterns.some((regex) => regex.test(textoLower))) { return tipo; } } return null; } function inferirTipoAtaque(args: RegistroEventoArgs): AtaqueCiberneticoTipo { if (args.tipoAtaque) return args.tipoAtaque; const corpus = [ args.descricao ?? '', args.mitreTechnique ?? '', args.tags?.join(' ') ?? '', ...(args.indicadores ?? []).map((i) => `${i.tipo}:${i.valor}`) ] .join(' ') .toLowerCase(); // Usar função de análise melhorada const tipoDetectado = analisarStringParaAtaques(corpus); if (tipoDetectado) { return tipoDetectado; } // Detecções baseadas em métricas e portas if ((args.metricas?.pps ?? 0) > 120_000) { return 'ddos'; } if ((args.destinoPorta === 3306 || args.destinoPorta === 1433) && corpus.includes('union')) { return 'sql_injection'; } if (corpus.includes('tls downgrade')) { return 'man_in_the_middle'; } return 'malware'; } function calcularSeveridade( tipo: AtaqueCiberneticoTipo, metricas?: RegistroMetricas, sugerida?: SeveridadeSeguranca ): SeveridadeSeguranca { if (sugerida) return sugerida; const baseScore = SEVERIDADE_SCORE[BASE_SEVERIDADE[tipo] ?? 'moderado']; let score = baseScore; if ((metricas?.pps ?? 0) > 250_000) score += 1; if ((metricas?.bps ?? 0) > 1_000_000_000) score += 1; if ((metricas?.hostsAfetados ?? 0) > 50) score += 1; if ((metricas?.rpm ?? 0) > 10_000) score += 1; const bounded = Math.min(Math.max(score, 0), 4); return SCORE_SEVERIDADE[bounded] ?? 'moderado'; } function statusInicial(severidade: SeveridadeSeguranca): StatusEventoSeguranca { if (severidade === 'critico') return 'escalado'; if (severidade === 'alto') return 'investigando'; return 'detectado'; } async function ajustarReputacao( ctx: MutationCtx, indicador: string, categoria: 'ip' | 'dominio' | 'hash' | 'email', delta: number, severidade: SeveridadeSeguranca, opcoes?: { blacklist?: boolean; whitelist?: boolean; bloqueadoAte?: number } ) { const existente = await ctx.db .query('ipReputation') .withIndex('by_indicador', (q) => q.eq('indicador', indicador)) .order('desc') .first(); if (existente) { await ctx.db.patch(existente._id, { reputacao: Math.max(-100, Math.min(100, existente.reputacao + delta)), ultimoRegistro: Date.now(), bloqueadoAte: opcoes?.bloqueadoAte ?? existente.bloqueadoAte, blacklist: opcoes?.blacklist ?? existente.blacklist, whitelist: opcoes?.whitelist ?? existente.whitelist, severidadeMax: SEVERIDADE_SCORE[severidade] > SEVERIDADE_SCORE[existente.severidadeMax] ? severidade : existente.severidadeMax, ocorrencias: existente.ocorrencias + 1 }); return existente._id; } return ctx.db.insert('ipReputation', { indicador, categoria, reputacao: Math.max(-100, Math.min(100, delta)), severidadeMax: severidade, whitelist: opcoes?.whitelist ?? false, blacklist: opcoes?.blacklist ?? false, ocorrencias: 1, primeiroRegistro: Date.now(), ultimoRegistro: Date.now(), bloqueadoAte: opcoes?.bloqueadoAte, origem: 'detector', comentarios: undefined, classificacoes: undefined, ultimaAcaoId: undefined }); } export const registrarEventoSeguranca = mutation({ args: { referencia: v.string(), sensorId: v.optional(v.id('networkSensors')), descricao: v.optional(v.string()), tipoAtaque: v.optional(ataqueValidator), severidade: v.optional(severidadeValidator), origemIp: v.optional(v.string()), destinoIp: v.optional(v.string()), destinoPorta: v.optional(v.number()), protocolo: v.optional(v.string()), transporte: v.optional(v.string()), origemRegiao: v.optional(v.string()), origemAsn: v.optional(v.string()), mitreTechnique: v.optional(v.string()), indicadores: v.optional( v.array( v.object({ tipo: v.string(), valor: v.string(), confianca: v.optional(v.number()) }) ) ), metricas: v.optional( v.object({ pps: v.optional(v.number()), bps: v.optional(v.number()), rpm: v.optional(v.number()), errosPorSegundo: v.optional(v.number()), hostsAfetados: v.optional(v.number()) }) ), fingerprint: v.optional( v.object({ userAgent: v.optional(v.string()), deviceId: v.optional(v.string()), ja3: v.optional(v.string()), tlsVersion: v.optional(v.string()) }) ), geolocalizacao: v.optional( v.object({ pais: v.optional(v.string()), regiao: v.optional(v.string()), cidade: v.optional(v.string()), latitude: v.optional(v.number()), longitude: v.optional(v.number()) }) ), tags: v.optional(v.array(v.string())) }, returns: v.object({ eventoId: v.id('securityEvents'), severidade: severidadeValidator, novoRegistro: v.boolean() }), handler: async (ctx, args) => { // Aplicar rate limiting por IP se fornecido if (args.origemIp) { 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 status = statusInicial(severidade); const duplicado = await ctx.db .query('securityEvents') .withIndex('by_referencia', (q) => q.eq('referencia', args.referencia)) .order('desc') .first(); if (duplicado) { await ctx.db.patch(duplicado._id, { severidade, status, atualizadoEm: Date.now(), descricao: args.descricao ?? duplicado.descricao, metricas: args.metricas ?? duplicado.metricas, tags: args.tags ?? duplicado.tags }); return { eventoId: duplicado._id, severidade, novoRegistro: false }; } const eventoId = await ctx.db.insert('securityEvents', { referencia: args.referencia, timestamp: Date.now(), tipoAtaque: tipo, severidade, status, descricao: args.descricao ?? `Evento registrado automaticamente para ${tipo.toUpperCase().replace(/_/g, ' ')}.`, origemIp: args.origemIp, destinoIp: args.destinoIp, destinoPorta: args.destinoPorta, protocolo: args.protocolo, transporte: args.transporte, sensorId: args.sensorId, detectadoPor: args.sensorId ? 'sensor' : 'manual', mitreTechnique: args.mitreTechnique, origemRegiao: args.origemRegiao, origemAsn: args.origemAsn, geolocalizacao: args.geolocalizacao, fingerprint: args.fingerprint, indicadores: args.indicadores, metricas: args.metricas, tags: args.tags, referenciasExternas: undefined, correlacoes: undefined, criadoPor: undefined, atualizadoEm: Date.now() }); if (args.origemIp) { const delta = CRITICAS.has(severidade) ? -30 : -10; const bloqueadoAte = CRITICAS.has(severidade) ? Date.now() + 60 * 60 * 1000 : undefined; await ajustarReputacao(ctx, args.origemIp, 'ip', delta, severidade, { blacklist: CRITICAS.has(severidade), bloqueadoAte }); } if (CRITICAS.has(severidade)) { await ctx.scheduler.runAfter(0, internal.security.dispararAlertasInternos, { eventoId }); } return { eventoId, severidade, novoRegistro: true }; } }); export const listarEventosSeguranca = query({ args: { limit: v.optional(v.number()), apos: v.optional(v.number()), severidades: v.optional(v.array(severidadeValidator)), tiposAtaque: v.optional(v.array(ataqueValidator)), status: v.optional(v.array(statusValidator)) }, returns: v.array( v.object({ _id: v.id('securityEvents'), timestamp: v.number(), tipoAtaque: ataqueValidator, severidade: severidadeValidator, status: statusValidator, descricao: v.string(), origemIp: v.optional(v.string()), destinoIp: v.optional(v.string()), destinoPorta: v.optional(v.number()), protocolo: v.optional(v.string()), tags: v.optional(v.array(v.string())) }) ), handler: async (ctx, args) => { await assertAdmin(ctx); const limit = args.limit && args.limit > 0 ? Math.min(args.limit, 500) : 100; const janelaInicial = args.apos ?? Date.now() - 6 * 60 * 60 * 1000; const { severidades, tiposAtaque, status } = args; let builder; if (severidades && severidades.length === 1) { const [severidade] = severidades; builder = ctx.db .query('securityEvents') .withIndex('by_severidade', (q) => q.eq('severidade', severidade)); } else if (tiposAtaque && tiposAtaque.length === 1) { const [tipoAtaque] = tiposAtaque; builder = ctx.db .query('securityEvents') .withIndex('by_tipo', (q) => q.eq('tipoAtaque', tipoAtaque)); } else if (status && status.length === 1) { const [estado] = status; builder = ctx.db .query('securityEvents') .withIndex('by_status', (q) => q.eq('status', estado)); } else { builder = ctx.db .query('securityEvents') .withIndex('by_timestamp', (q) => q.gte('timestamp', janelaInicial)); } 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) ) { return false; } 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)) { return false; } return true; }) .slice(0, limit); return filtrados.map((evento) => ({ _id: evento._id, timestamp: evento.timestamp, tipoAtaque: evento.tipoAtaque, severidade: evento.severidade, status: evento.status, descricao: evento.descricao, origemIp: evento.origemIp, destinoIp: evento.destinoIp, destinoPorta: evento.destinoPorta, protocolo: evento.protocolo, tags: evento.tags })); } }); const ATAQUES_AVANCADOS: ReadonlySet = new Set([ 'apt', 'zero_day', 'supply_chain', 'fileless_malware', 'polymorphic_malware', 'ransomware_lateral', 'deepfake_phishing', 'adversarial_ai', 'side_channel', 'firmware_bootloader', 'botnet', 'ot_ics', 'quantum_attack' ]); export const obterVisaoCamadas = query({ args: { periodoHoras: v.optional(v.number()), buckets: v.optional(v.number()) }, returns: v.object({ series: v.array( v.object({ bucket: v.number(), inicio: v.number(), fim: v.number(), bloqueios: v.number(), ddos: v.number(), sqlInjection: v.number(), phishing: v.number(), avancados: v.number() }) ), totais: v.object({ eventos: v.number(), criticos: v.number(), bloqueiosAtivos: v.number(), sensoresAtivos: v.number() }) }), handler: async (ctx, args) => { await assertAdmin(ctx); const agora = Date.now(); const periodoMs = Math.max(1, args.periodoHoras ?? 6) * 60 * 60 * 1000; const inicioJanela = agora - periodoMs; const bucketCount = Math.min(Math.max(args.buckets ?? 24, 4), 96); const bucketSize = Math.ceil(periodoMs / bucketCount); const eventos = await ctx.db .query('securityEvents') .withIndex('by_timestamp', (q) => q.gte('timestamp', inicioJanela)) .order('asc') .collect(); const series = Array.from({ length: bucketCount }, (_, index) => { const inicio = inicioJanela + index * bucketSize; return { bucket: index, inicio, fim: inicio + bucketSize, bloqueios: 0, ddos: 0, sqlInjection: 0, phishing: 0, avancados: 0 }; }); let criticos = 0; for (const evento of eventos) { const idx = Math.min( bucketCount - 1, Math.max(0, Math.floor((evento.timestamp - inicioJanela) / bucketSize)) ); const bucket = series[idx]; if (evento.severidade === 'critico') criticos += 1; if (CRITICAS.has(evento.severidade)) bucket.bloqueios += 1; if (evento.tipoAtaque === 'ddos') bucket.ddos += 1; if (evento.tipoAtaque === 'sql_injection') bucket.sqlInjection += 1; if (evento.tipoAtaque === 'phishing' || evento.tipoAtaque === 'deepfake_phishing') { bucket.phishing += 1; } if (ATAQUES_AVANCADOS.has(evento.tipoAtaque)) bucket.avancados += 1; } const bloqueios = await ctx.db .query('ipReputation') .withIndex('by_blacklist', (q) => q.eq('blacklist', true)) .collect(); const bloqueiosAtivos = bloqueios.filter( (item) => !item.bloqueadoAte || item.bloqueadoAte > agora ).length; const sensoresAtivos = await ctx.db .query('networkSensors') .withIndex('by_status', (q) => q.eq('status', 'ativo')) .collect(); return { series, totais: { eventos: eventos.length, criticos, bloqueiosAtivos, sensoresAtivos: sensoresAtivos.length } }; } }); export const listarReputacoes = query({ args: { limit: v.optional(v.number()), categoria: v.optional(indicadorCategoriaValidator), lista: v.optional(v.union(v.literal('blacklist'), v.literal('whitelist'))) }, returns: v.array( v.object({ _id: v.id('ipReputation'), indicador: v.string(), categoria: indicadorCategoriaValidator, reputacao: v.number(), severidadeMax: severidadeValidator, whitelist: v.boolean(), blacklist: v.boolean(), bloqueadoAte: v.optional(v.number()), ocorrencias: v.number(), ultimoRegistro: v.number() }) ), handler: async (ctx, args) => { await assertAdmin(ctx); 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 docs = await builder.order('desc').take(limit * 2); const filtrados = docs .filter((doc) => { if (args.categoria && doc.categoria !== args.categoria) return false; return true; }) .slice(0, limit); return filtrados.map((doc) => ({ _id: doc._id, indicador: doc.indicador, categoria: doc.categoria, reputacao: doc.reputacao, severidadeMax: doc.severidadeMax, whitelist: doc.whitelist, blacklist: doc.blacklist, bloqueadoAte: doc.bloqueadoAte, ocorrencias: doc.ocorrencias, ultimoRegistro: doc.ultimoRegistro })); } }); export const atualizarReputacaoIndicador = mutation({ args: { indicador: v.string(), categoria: indicadorCategoriaValidator, acao: v.union( v.literal('forcar_blacklist'), v.literal('remover_blacklist'), v.literal('forcar_whitelist'), v.literal('remover_whitelist'), v.literal('ajustar_score'), v.literal('registrar_comentario') ), delta: v.optional(v.number()), comentario: v.optional(v.string()), duracaoSegundos: v.optional(v.number()) }, returns: v.object({ reputacaoId: v.id('ipReputation'), status: v.string() }), handler: async (ctx, args) => { const usuarioAtual = await assertAdmin(ctx); // Aplicar rate limiting por usuário const rateLimitResult = await aplicarRateLimit( ctx, 'usuario', usuarioAtual._id, 'atualizarReputacaoIndicador' ); if (!rateLimitResult.permitido) { throw new Error(rateLimitResult.motivo ?? 'Rate limit excedido'); } const existente = await ctx.db .query('ipReputation') .withIndex('by_indicador', (q) => q.eq('indicador', args.indicador)) .order('desc') .first(); const agora = Date.now(); const bloqueioAte = args.duracaoSegundos ? agora + args.duracaoSegundos * 1000 : undefined; if (!existente) { const reputacaoId = await ctx.db.insert('ipReputation', { indicador: args.indicador, categoria: args.categoria, reputacao: args.delta ?? 0, severidadeMax: 'moderado', whitelist: args.acao === 'forcar_whitelist', blacklist: args.acao === 'forcar_blacklist', ocorrencias: 1, primeiroRegistro: agora, ultimoRegistro: agora, bloqueadoAte: bloqueioAte, origem: 'painel', comentarios: args.comentario, classificacoes: undefined, ultimaAcaoId: undefined }); await ctx.db.insert('incidentActions', { eventoId: await ctx.db.insert('securityEvents', { referencia: `auto-${args.indicador}-${agora}`, timestamp: agora, tipoAtaque: 'malware', severidade: 'informativo', status: 'contido', descricao: 'Registro criado via painel de reputação.', origemIp: args.categoria === 'ip' ? args.indicador : undefined, destinoIp: undefined, destinoPorta: undefined, protocolo: undefined, transporte: undefined, sensorId: undefined, detectadoPor: 'manual', mitreTechnique: undefined, origemRegiao: undefined, origemAsn: undefined, geolocalizacao: undefined, fingerprint: undefined, indicadores: undefined, metricas: undefined, tags: ['reputacao'], referenciasExternas: undefined, correlacoes: undefined, criadoPor: usuarioAtual._id, atualizadoEm: agora }), tipo: args.acao === 'forcar_blacklist' ? 'block_ip' : 'custom', origem: 'manual', status: 'concluido', executadoPor: usuarioAtual._id, detalhes: args.comentario, resultado: 'Registro inicial', relacionadoA: undefined, criadoEm: agora, atualizadoEm: agora }); return { reputacaoId, status: 'criado' }; } const patch: Record = { ultimoRegistro: agora }; switch (args.acao) { case 'forcar_blacklist': patch.blacklist = true; patch.bloqueadoAte = bloqueioAte; break; case 'remover_blacklist': patch.blacklist = false; patch.bloqueadoAte = undefined; break; case 'forcar_whitelist': patch.whitelist = true; break; case 'remover_whitelist': patch.whitelist = false; break; case 'ajustar_score': patch.reputacao = Math.max( -100, Math.min(100, (existente.reputacao ?? 0) + (args.delta ?? 0)) ); break; case 'registrar_comentario': patch.comentarios = args.comentario; break; } await ctx.db.patch(existente._id, patch); return { reputacaoId: existente._id, status: 'atualizado' }; } }); export const configurarRegraPorta = mutation({ args: { regraId: v.optional(v.id('portRules')), porta: v.number(), protocolo: protocoloValidator, acao: portActionValidator, temporario: v.boolean(), duracaoSegundos: v.optional(v.number()), severidadeMin: severidadeValidator, notas: v.optional(v.string()), tags: v.optional(v.array(v.string())), listaReferencia: v.optional(v.id('ipReputation')) }, returns: v.object({ regraId: v.id('portRules'), status: v.string() }), handler: async (ctx, args) => { const usuarioAtual = await assertAdmin(ctx); const agora = Date.now(); const expiraEm = args.temporario && args.duracaoSegundos ? agora + args.duracaoSegundos * 1000 : undefined; if (args.regraId) { await ctx.db.patch(args.regraId, { porta: args.porta, protocolo: args.protocolo, acao: args.acao, temporario: args.temporario, duracaoSegundos: args.duracaoSegundos, expiraEm, atualizadoPor: usuarioAtual._id, atualizadoEm: agora, severidadeMin: args.severidadeMin, notas: args.notas, tags: args.tags, listaReferencia: args.listaReferencia }); return { regraId: args.regraId, status: 'atualizado' }; } const regraId = await ctx.db.insert('portRules', { porta: args.porta, protocolo: args.protocolo, acao: args.acao, temporario: args.temporario, severidadeMin: args.severidadeMin, duracaoSegundos: args.duracaoSegundos, expiraEm, criadoPor: usuarioAtual._id, atualizadoPor: usuarioAtual._id, criadoEm: agora, atualizadoEm: agora, notas: args.notas, tags: args.tags, listaReferencia: args.listaReferencia }); return { regraId, status: 'criado' }; } }); export const listarRegrasPorta = query({ args: { acao: v.optional(portActionValidator), ativo: v.optional(v.boolean()) }, returns: v.array( v.object({ _id: v.id('portRules'), porta: v.number(), protocolo: protocoloValidator, acao: portActionValidator, temporario: v.boolean(), expiraEm: v.optional(v.number()), severidadeMin: severidadeValidator, tags: v.optional(v.array(v.string())), notas: v.optional(v.string()) }) ), handler: async (ctx, args) => { await assertAdmin(ctx); const builder = args.acao === undefined ? ctx.db.query('portRules') : ctx.db.query('portRules').withIndex('by_acao', (q) => { const acao = args.acao; return q.eq('acao', acao as 'permitir' | 'bloquear' | 'monitorar' | 'rate_limit'); }); const docs = await builder.order('desc').take(200); const filtrados = docs.filter((doc) => { if (args.ativo === undefined) return true; if (!doc.temporario) return true; if (!doc.expiraEm) return !args.ativo; const aindaValido = doc.expiraEm > Date.now(); return args.ativo ? aindaValido : !aindaValido; }); return filtrados.map((doc) => ({ _id: doc._id, porta: doc.porta, protocolo: doc.protocolo, acao: doc.acao, temporario: doc.temporario, expiraEm: doc.expiraEm, severidadeMin: doc.severidadeMin, tags: doc.tags, notas: doc.notas })); } }); export const registrarAcaoIncidente = mutation({ args: { eventoId: v.id('securityEvents'), 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') ) ), detalhes: v.optional(v.string()), resultado: v.optional(v.string()), relacionadoA: v.optional(v.id('ipReputation')) }, returns: v.object({ acaoId: v.id('incidentActions') }), handler: async (ctx, args) => { const usuarioAtual = await assertAdmin(ctx); if (args.executadoPor && args.executadoPor !== usuarioAtual._id) { throw new Error('acesso_negado'); } const agora = Date.now(); const acaoId = await ctx.db.insert('incidentActions', { eventoId: args.eventoId, tipo: args.tipo, origem: args.origem, status: args.status ?? 'concluido', executadoPor: usuarioAtual._id, detalhes: args.detalhes, resultado: args.resultado, relacionadoA: args.relacionadoA, criadoEm: agora, atualizadoEm: agora }); return { acaoId }; } }); export const solicitarRelatorioSeguranca = mutation({ args: { filtros: v.object({ dataInicio: v.number(), dataFim: v.number(), severidades: v.optional(v.array(severidadeValidator)), tiposAtaque: v.optional(v.array(ataqueValidator)), incluirIndicadores: v.optional(v.boolean()), incluirMetricas: v.optional(v.boolean()), incluirAcoes: v.optional(v.boolean()) }) }, returns: v.object({ relatorioId: v.id('reportRequests') }), handler: async (ctx, args) => { const usuarioAtual = await assertAdmin(ctx); const relatorioId = await ctx.db.insert('reportRequests', { solicitanteId: usuarioAtual._id, filtros: { dataInicio: args.filtros.dataInicio, dataFim: args.filtros.dataFim, severidades: args.filtros.severidades, tiposAtaque: args.filtros.tiposAtaque, incluirIndicadores: args.filtros.incluirIndicadores, incluirMetricas: args.filtros.incluirMetricas, incluirAcoes: args.filtros.incluirAcoes }, status: 'pendente', resultadoId: undefined, observacoes: undefined, criadoEm: Date.now(), atualizadoEm: Date.now(), concluidoEm: undefined, erro: undefined }); await ctx.scheduler.runAfter(0, internal.security.processarRelatorioSegurancaInternal, { relatorioId }); return { relatorioId }; } }); // Lista relatórios recentes para exibição no dashboard export const listarRelatoriosRecentes = query({ args: { limit: v.optional(v.number()) }, returns: v.array( v.object({ _id: v.id('reportRequests'), status: v.union( v.literal('pendente'), v.literal('processando'), v.literal('concluido'), v.literal('falhou') ), criadoEm: v.number(), concluidoEm: v.optional(v.number()), observacoes: v.optional(v.string()) }) ), handler: async (ctx, args) => { await assertAdmin(ctx); const max = Math.min(args.limit ?? 10, 50); const rows = await ctx.db .query('reportRequests') .withIndex('by_criado_em', (q) => q.gte('criadoEm', 0)) .order('desc') .take(max); return rows.map((r) => ({ _id: r._id, status: r.status, criadoEm: r.criadoEm, concluidoEm: r.concluidoEm, observacoes: r.observacoes })); } }); // Health check leve para o dashboard export const healthStatus = query({ args: {}, returns: v.object({ ok: v.boolean(), now: v.number(), pendingReports: v.number() }), handler: async (ctx) => { await assertAdmin(ctx); // Contar rapidamente quantos relatórios pendentes existem (limitado) const pending = await ctx.db .query('reportRequests') .withIndex('by_status', (q) => q.eq('status', 'pendente')) .take(1); return { ok: true, now: Date.now(), pendingReports: pending.length }; } }); /** * Excluir relatório gerado (e artefato, se houver) */ export const deletarRelatorio = mutation({ args: { relatorioId: v.id('reportRequests') }, returns: v.object({ success: v.boolean() }), handler: async (ctx, args) => { await assertAdmin(ctx); const doc = await ctx.db.get(args.relatorioId); if (!doc) { return { success: false }; } // Remover arquivo em storage se existir if (doc.resultadoId) { try { await ctx.storage.delete(doc.resultadoId); } catch { // Ignorar falha ao excluir artefato de storage } } await ctx.db.delete(args.relatorioId); return { success: true }; } }); export const processarRelatorioSegurancaInternal = internalMutation({ args: { relatorioId: v.id('reportRequests') }, returns: v.null(), handler: async (ctx, args) => { const relatorio = await ctx.db.get(args.relatorioId); if (!relatorio || relatorio.status !== 'pendente') return null; const eventos = await ctx.db .query('securityEvents') .withIndex('by_timestamp', (q) => q.gte('timestamp', relatorio.filtros.dataInicio).lte('timestamp', relatorio.filtros.dataFim) ) .collect(); const filtrados = eventos.filter((evento) => { if ( relatorio.filtros.severidades && relatorio.filtros.severidades.length > 0 && !relatorio.filtros.severidades.includes(evento.severidade) ) { return false; } if ( relatorio.filtros.tiposAtaque && relatorio.filtros.tiposAtaque.length > 0 && !relatorio.filtros.tiposAtaque.includes(evento.tipoAtaque) ) { return false; } return true; }); const porSeveridade = filtrados.reduce>( (acc, evento) => { acc[evento.severidade] = (acc[evento.severidade] ?? 0) + 1; return acc; }, { informativo: 0, baixo: 0, moderado: 0, alto: 0, critico: 0 } ); const porAtaque = filtrados.reduce>((acc, evento) => { acc[evento.tipoAtaque] = (acc[evento.tipoAtaque] ?? 0) + 1; return acc; }, {}); const observacoes = JSON.stringify({ total: filtrados.length, porSeveridade, porAtaque, incluiuIndicadores: relatorio.filtros.incluirIndicadores ?? false, incluiuMetricas: relatorio.filtros.incluirMetricas ?? false, incluiuAcoes: relatorio.filtros.incluirAcoes ?? false }); await ctx.db.patch(relatorio._id, { status: 'concluido', observacoes, atualizadoEm: Date.now(), concluidoEm: Date.now() }); return null; } }); // ---------- Alertas (email/chat) ---------- export const listarAlertConfigs = query({ args: { limit: v.optional(v.number()) }, returns: v.array( v.object({ _id: v.id('alertConfigs'), nome: v.string(), canais: v.object({ email: v.boolean(), chat: v.boolean() }), emails: v.array(v.string()), chatUsers: v.array(v.string()), severidadeMin: severidadeValidator, tiposAtaque: v.optional(v.array(ataqueValidator)), reenvioMin: v.number(), templateCodigo: v.optional(v.string()), criadoEm: v.number(), atualizadoEm: v.number() }) ), handler: async (ctx, args) => { await assertAdmin(ctx); const max = Math.min(args.limit ?? 100, 200); const rows = await ctx.db .query('alertConfigs') .withIndex('by_criadoEm', (q) => q.gte('criadoEm', 0)) .order('desc') .take(max); return rows.map((r) => ({ _id: r._id, nome: r.nome, canais: r.canais, emails: r.emails, chatUsers: r.chatUsers, severidadeMin: r.severidadeMin, tiposAtaque: r.tiposAtaque, reenvioMin: r.reenvioMin, templateCodigo: r.templateCodigo, criadoEm: r.criadoEm, atualizadoEm: r.atualizadoEm })); } }); export const salvarAlertConfig = mutation({ args: { configId: v.optional(v.id('alertConfigs')), nome: v.string(), canais: v.object({ email: v.boolean(), chat: v.boolean() }), emails: v.array(v.string()), chatUsers: v.array(v.string()), severidadeMin: severidadeValidator, tiposAtaque: v.optional(v.array(ataqueValidator)), reenvioMin: v.number(), templateCodigo: v.optional(v.string()) // Template a ser usado }, returns: v.object({ _id: v.id('alertConfigs') }), handler: async (ctx, args) => { const usuarioAtual = await assertAdmin(ctx); const agora = Date.now(); if (args.configId) { await ctx.db.patch(args.configId, { nome: args.nome, canais: args.canais, emails: args.emails, chatUsers: args.chatUsers, severidadeMin: args.severidadeMin, tiposAtaque: args.tiposAtaque, reenvioMin: args.reenvioMin, templateCodigo: args.templateCodigo, atualizadoEm: agora }); return { _id: args.configId }; } const id = await ctx.db.insert('alertConfigs', { nome: args.nome, canais: args.canais, emails: args.emails, chatUsers: args.chatUsers, severidadeMin: args.severidadeMin, tiposAtaque: args.tiposAtaque, reenvioMin: args.reenvioMin, templateCodigo: args.templateCodigo ?? 'incidente_critico', // Padrão criadoPor: usuarioAtual._id, criadoEm: agora, atualizadoEm: agora }); return { _id: id }; } }); export const deletarAlertConfig = mutation({ args: { configId: v.id('alertConfigs') }, returns: v.null(), handler: async (ctx, args) => { await assertAdmin(ctx); await ctx.db.delete(args.configId); return null; } }); export const dispararAlertasInternos = internalMutation({ args: { eventoId: v.id('securityEvents') }, returns: v.null(), handler: async (ctx, args) => { 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 = { 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', 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)) .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); } } for (const usuarioId of usuariosNotificados) { await ctx.db.insert('notificacoes', { usuarioId, tipo: 'alerta_seguranca', conversaId: undefined, mensagemId: undefined, remetenteId: undefined, titulo: `🚨 ${evento.severidade.toUpperCase()} - ${tipoAtaqueLabel}`, descricao: evento.descricao, lida: false, criadaEm: Date.now() }); } return null; } }); /** * 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(), handler: async (ctx) => { const agora = Date.now(); const bloqueios = await ctx.db .query('ipReputation') .withIndex('by_blacklist', (q) => q.eq('blacklist', true)) .collect(); for (const registro of bloqueios) { if (registro.bloqueadoAte && registro.bloqueadoAte <= agora) { await ctx.db.patch(registro._id, { blacklist: false, bloqueadoAte: undefined }); } } const regras = await ctx.db .query('portRules') .withIndex('by_expiracao', (q) => q.lte('expiraEm', agora)) .collect(); for (const regra of regras) { if (regra.temporario && regra.expiraEm && regra.expiraEm <= agora) { await ctx.db.patch(regra._id, { acao: 'monitorar', temporario: false, expiraEm: undefined }); } } return null; } }); export const atualizarThreatIntelFeedsInternal = internalMutation({ args: {}, returns: v.null(), handler: async (ctx) => { const feeds = await ctx.db .query('threatIntelFeeds') .withIndex('by_ativo', (q) => q.eq('ativo', true)) .collect(); for (const feed of feeds) { const entradasSimuladas = Math.floor(Math.random() * 25) + 5; await ctx.db.patch(feed._id, { ultimaSincronizacao: Date.now(), entradasProcessadas: (feed.entradasProcessadas ?? 0) + entradasSimuladas, errosConsecutivos: 0 }); } return null; } }); // Helper para aplicar rate limiting baseado em configurações usando @convex-dev/rate-limiter async function aplicarRateLimit( ctx: QueryCtx | MutationCtx, tipo: 'ip' | 'usuario' | 'endpoint' | 'global', identificador: string, endpoint?: string ): 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) ) .filter((q) => q.eq(q.field('ativo'), true)) .collect(); let config; if (configs.length === 0) { // Verificar configuração global const globalConfigs = await ctx.db .query('rateLimitConfig') .withIndex('by_tipo_identificador', (q) => q.eq('tipo', 'global').eq('identificador', 'global') ) .filter((q) => q.eq(q.field('ativo'), true)) .collect(); if (globalConfigs.length === 0) { return { permitido: true }; } config = globalConfigs.sort((a, b) => b.prioridade - a.prioridade)[0]; } else { config = configs.sort((a, b) => b.prioridade - a.prioridade)[0]; } // 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'; // Criar namespace único para este rate limit const namespace = `${tipo}:${identificador}:${endpoint ?? 'default'}`; // Criar instância do RateLimiter com configuração dinâmica // Usar type assertion para permitir configuração dinâmica const rateLimiterConfig = { [namespace]: { kind, rate: config.limite, period: periodo, ...(config.estrategia === 'token_bucket' ? { capacity: config.limite } : {}) } } as Record< string, { kind: 'token bucket' | 'fixed window'; rate: number; period: number; capacity?: number; } >; const rateLimiter = new RateLimiter(components.rateLimiter, rateLimiterConfig); // Verificar rate limit const result = await rateLimiter.check(ctx, namespace, { key: identificador }); 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, motivo: `Bloqueado por rate limit: ${config.limite} requisições por ${config.janelaSegundos}s`, retryAfter }; } else if (config.acaoExcedido === 'alertar') { // Permitir mas registrar alerta return { permitido: true }; } // throttle - permitir mas com delay return { permitido: true, retryAfter }; } // Aplicar o limite (consumir token) apenas em mutations // Verificar se é MutationCtx tentando usar limit (que só funciona em mutations) if ('runMutation' in ctx) { await rateLimiter.limit(ctx as MutationCtx, namespace, { key: identificador, throws: false }); } return { permitido: true }; } export const criarConfigRateLimit = mutation({ args: { 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()), prioridade: v.optional(v.number()), notas: v.optional(v.string()), tags: v.optional(v.array(v.string())) }, returns: v.id('rateLimitConfig'), handler: async (ctx, args) => { const usuarioAtual = await assertAdmin(ctx); const agora = Date.now(); const configId = await ctx.db.insert('rateLimitConfig', { nome: args.nome, tipo: args.tipo, identificador: args.identificador ?? (args.tipo === 'global' ? 'global' : undefined), limite: args.limite, janelaSegundos: args.janelaSegundos, estrategia: args.estrategia, acaoExcedido: args.acaoExcedido, bloqueioTemporarioSegundos: args.bloqueioTemporarioSegundos, ativo: true, prioridade: args.prioridade ?? 0, criadoPor: usuarioAtual._id, atualizadoPor: undefined, criadoEm: agora, atualizadoEm: agora, notas: args.notas, tags: args.tags }); return configId; } }); export const atualizarConfigRateLimit = mutation({ args: { configId: v.id('rateLimitConfig'), nome: v.optional(v.string()), 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')) ), 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()), notas: v.optional(v.string()), tags: v.optional(v.array(v.string())) }, returns: v.null(), handler: async (ctx, args) => { const usuarioAtual = await assertAdmin(ctx); const config = await ctx.db.get(args.configId); if (!config) { throw new Error('Configuração de rate limit não encontrada'); } const atualizacoes: { nome?: string; limite?: number; janelaSegundos?: number; estrategia?: 'fixed_window' | 'sliding_window' | 'token_bucket'; acaoExcedido?: 'bloquear' | 'throttle' | 'alertar'; bloqueioTemporarioSegundos?: number; ativo?: boolean; prioridade?: number; notas?: string; tags?: string[]; atualizadoPor: Id<'usuarios'>; atualizadoEm: number; } = { atualizadoPor: usuarioAtual._id, atualizadoEm: Date.now() }; if (args.nome !== undefined) atualizacoes.nome = args.nome; if (args.limite !== undefined) atualizacoes.limite = args.limite; if (args.janelaSegundos !== undefined) atualizacoes.janelaSegundos = args.janelaSegundos; if (args.estrategia !== undefined) atualizacoes.estrategia = args.estrategia; if (args.acaoExcedido !== undefined) atualizacoes.acaoExcedido = args.acaoExcedido; if (args.bloqueioTemporarioSegundos !== undefined) atualizacoes.bloqueioTemporarioSegundos = args.bloqueioTemporarioSegundos; if (args.ativo !== undefined) atualizacoes.ativo = args.ativo; if (args.prioridade !== undefined) atualizacoes.prioridade = args.prioridade; if (args.notas !== undefined) atualizacoes.notas = args.notas; if (args.tags !== undefined) atualizacoes.tags = args.tags; await ctx.db.patch(args.configId, atualizacoes); return null; } }); export const listarConfigsRateLimit = query({ args: { 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()) }, returns: v.array( v.object({ _id: v.id('rateLimitConfig'), 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(), notas: v.optional(v.string()), tags: v.optional(v.array(v.string())) }) ), handler: async (ctx, args) => { await assertAdmin(ctx); let builder; if (args.tipo !== undefined) { const tipo = args.tipo; builder = ctx.db .query('rateLimitConfig') .withIndex('by_tipo_identificador', (q) => q.eq('tipo', tipo)); } else if (args.ativo !== undefined) { const ativo = args.ativo; builder = ctx.db.query('rateLimitConfig').withIndex('by_ativo', (q) => q.eq('ativo', ativo)); } else { builder = ctx.db.query('rateLimitConfig'); } const configs = await builder.order('desc').take(args.limit ?? 100); return configs.map((config) => ({ _id: config._id, nome: config.nome, tipo: config.tipo, identificador: config.identificador, limite: config.limite, janelaSegundos: config.janelaSegundos, estrategia: config.estrategia, acaoExcedido: config.acaoExcedido, bloqueioTemporarioSegundos: config.bloqueioTemporarioSegundos, ativo: config.ativo, prioridade: config.prioridade, notas: config.notas, tags: config.tags })); } }); // ============================================ // DETECÇÃO AUTOMÁTICA DE ATAQUES // ============================================ /** * Analisa uma requisição HTTP e detecta possíveis ataques * Esta função pode ser chamada de interceptores HTTP ou middlewares */ export const analisarRequisicaoHTTP = mutation({ args: { url: v.string(), method: v.string(), headers: v.optional(v.record(v.string(), v.string())), body: v.optional(v.string()), queryParams: v.optional(v.record(v.string(), v.string())), ipOrigem: v.optional(v.string()), userAgent: v.optional(v.string()) }, returns: v.object({ ataqueDetectado: v.boolean(), tipoAtaque: v.optional(ataqueValidator), severidade: v.optional(severidadeValidator), eventoId: v.optional(v.id('securityEvents')), bloqueadoAutomatico: v.optional(v.boolean()) }), handler: async (ctx, args) => { // Combinar todos os dados da requisição para análise const dadosParaAnalise = [ 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'), args.userAgent ?? '' ].join('\n'); // Detectar tipo de ataque const tipoAtaque = analisarStringParaAtaques(dadosParaAnalise); if (!tipoAtaque) { return { ataqueDetectado: false, tipoAtaque: undefined, severidade: undefined, eventoId: undefined, bloqueadoAutomatico: undefined }; } // Calcular severidade const severidade = calcularSeveridade(tipoAtaque, undefined, undefined); // Verificar configuração de bloqueio automático const autoBlockConfig = await ctx.db .query('autoBlockConfig') .withIndex('by_tipo', (q) => q.eq('tipoAtaque', tipoAtaque)) .filter((q) => q.eq(q.field('ativo'), true)) .first(); let bloqueadoAutomatico = false; let bloqueadoAte: number | undefined = undefined; // Aplicar bloqueio automático se configurado if ( autoBlockConfig && autoBlockConfig.bloquearAutomatico && SEVERIDADE_SCORE[severidade] >= SEVERIDADE_SCORE[autoBlockConfig.severidadeMinima] ) { bloqueadoAutomatico = true; // Calcular data de expiração se duração definida if (autoBlockConfig.duracaoBloqueioSegundos) { bloqueadoAte = Date.now() + autoBlockConfig.duracaoBloqueioSegundos * 1000; } } // 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'])) || undefined; const protocolo = (args.queryParams && (args.queryParams['proto'] as string)) || 'http'; // Registrar evento de segurança const referencia = `http_${Date.now()}_${Math.random().toString(36).substring(7)}`; const agora = Date.now(); const tags = ['detecção_automática', 'http', tipoAtaque]; if (bloqueadoAutomatico) { tags.push('bloqueio_automatico'); } const eventoId = await ctx.db.insert('securityEvents', { referencia, timestamp: agora, tipoAtaque, severidade, status: statusInicial(severidade), descricao: `Ataque ${tipoAtaque} detectado na requisição HTTP ${args.method} ${args.url}${ bloqueadoAutomatico ? ' (BLOQUEADO AUTOMATICAMENTE)' : '' }`, origemIp: args.ipOrigem, protocolo, transporte: 'tcp', detectadoPor: 'analisador_http_automatico', fingerprint: args.userAgent ? { userAgent: args.userAgent } : undefined, destinoIp: destinoIp ?? undefined, tags, atualizadoEm: agora }); // Ajustar reputação do IP se fornecido if (args.ipOrigem) { const delta = SEVERIDADE_SCORE[severidade] * -10; // Penalidade baseada na severidade // Se bloqueio automático ativo, aplicar blacklist const opcoesBlacklist = bloqueadoAutomatico ? { blacklist: true, bloqueadoAte } : severidade === 'critico' || severidade === 'alto' ? { blacklist: true } : undefined; await ajustarReputacao(ctx, args.ipOrigem, 'ip', delta, severidade, opcoesBlacklist); } return { ataqueDetectado: true, tipoAtaque, severidade, eventoId, bloqueadoAutomatico }; } }); /** * Detecta ataques de brute force baseado em logs de login * Esta função deve ser chamada periodicamente ou após múltiplas tentativas falhas */ export const detectarBruteForce = internalMutation({ args: { ipAddress: v.optional(v.string()), usuarioId: v.optional(v.id('usuarios')), janelaMinutos: v.optional(v.number()) // padrão 15 minutos }, returns: v.object({ ataqueDetectado: v.boolean(), tentativasFalhas: v.number(), eventoId: v.optional(v.id('securityEvents')) }), handler: async (ctx, args) => { // Sensibilidade maior em dev: janela menor const janelaMinutos = args.janelaMinutos ?? 10; const dataLimite = Date.now() - janelaMinutos * 60 * 1000; // Buscar tentativas de login falhas let tentativasFalhas; if (args.ipAddress) { 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)) .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)) .collect(); } else { // Buscar todas as tentativas falhas na janela tentativasFalhas = await ctx.db .query('logsLogin') .withIndex('by_sucesso', (q) => q.eq('sucesso', false)) .filter((q) => q.gte(q.field('timestamp'), dataLimite)) .collect(); } // Agrupar por IP para detectar padrões const tentativasPorIP: Record = {}; tentativasFalhas.forEach((t) => { if (t.ipAddress) { tentativasPorIP[t.ipAddress] = (tentativasPorIP[t.ipAddress] || 0) + 1; } }); // Detectar IPs suspeitos (3+ tentativas falhas em dev) const ipsSuspeitos = Object.entries(tentativasPorIP) .filter(([, count]) => count >= 3) .map(([ip, count]) => ({ ip, count })); if (ipsSuspeitos.length === 0) { return { ataqueDetectado: false, tentativasFalhas: tentativasFalhas.length, eventoId: undefined }; } // Registrar eventos para cada IP suspeito const eventosIds: Id<'securityEvents'>[] = []; for (const { ip, count } of ipsSuspeitos) { const severidade: SeveridadeSeguranca = count >= 8 ? 'alto' : count >= 5 ? 'moderado' : 'baixo'; const referencia = `brute_force_${ip}_${Date.now()}`; const agora = Date.now(); const eventoId = await ctx.db.insert('securityEvents', { referencia, timestamp: agora, tipoAtaque: 'brute_force', severidade, status: statusInicial(severidade), descricao: `Ataque de brute force detectado: ${count} tentativas de login falhas do IP ${ip} em ${janelaMinutos} minutos`, origemIp: ip, protocolo: 'http', transporte: 'tcp', detectadoPor: 'detector_brute_force', metricas: { rpm: Math.round((count / janelaMinutos) * 60) }, tags: ['detecção_automática', 'brute_force', 'login'], atualizadoEm: agora }); eventosIds.push(eventoId); // Ajustar reputação do IP const delta = count * -5; // Penalidade baseada no número de tentativas await ajustarReputacao( ctx, ip, 'ip', delta, severidade, severidade === 'alto' ? { blacklist: true, bloqueadoAte: agora + 60 * 60 * 1000 // Bloquear por 1 hora } : undefined ); } return { ataqueDetectado: true, tentativasFalhas: tentativasFalhas.length, eventoId: eventosIds[0] // Retornar o primeiro evento criado }; } }); /** * Monitora logs de login e detecta brute force automaticamente * Esta função deve ser chamada periodicamente (ex: a cada 5 minutos) */ /** * Função de teste para criar eventos de segurança de exemplo * Útil para validar se o dashboard está funcionando corretamente */ export const criarEventosTeste = mutation({ args: { quantidade: v.optional(v.number()) }, returns: v.object({ eventosCriados: v.number(), eventosIds: v.array(v.id('securityEvents')) }), handler: async (ctx, args) => { assertDevOnly(); const quantidade = args.quantidade ?? 10; const eventosIds: Id<'securityEvents'>[] = []; const agora = Date.now(); // Tipos de ataque para teste const tiposAtaque: Array<{ tipo: AtaqueCiberneticoTipo; severidade: SeveridadeSeguranca; }> = [ { tipo: 'sql_injection', severidade: 'alto' }, { tipo: 'xss', severidade: 'moderado' }, { tipo: 'brute_force', severidade: 'alto' }, { tipo: 'path_traversal', severidade: 'alto' }, { tipo: 'command_injection', severidade: 'critico' }, { tipo: 'nosql_injection', severidade: 'alto' }, { tipo: 'xxe', severidade: 'alto' }, { tipo: 'ddos', severidade: 'critico' }, { tipo: 'phishing', severidade: 'moderado' }, { tipo: 'malware', severidade: 'alto' } ]; // 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']; for (let i = 0; i < quantidade; i++) { const tipoAtaque = tiposAtaque[i % tiposAtaque.length]; const ip = ipsTeste[i % ipsTeste.length]; const referencia = `teste_${Date.now()}_${i}_${Math.random().toString(36).substring(7)}`; const eventoId = await ctx.db.insert('securityEvents', { referencia, timestamp: agora - i * 60000, // Espaçar eventos em 1 minuto tipoAtaque: tipoAtaque.tipo, severidade: tipoAtaque.severidade, status: statusInicial(tipoAtaque.severidade), descricao: `[TESTE] Evento de teste: ${tipoAtaque.tipo} detectado do IP ${ip}. Este é um evento gerado para validação do sistema.`, origemIp: ip, destinoIp: '192.168.1.1', destinoPorta: 443, protocolo: 'http', transporte: 'tcp', detectadoPor: 'sistema_teste', metricas: { rpm: Math.floor(Math.random() * 1000), pps: Math.floor(Math.random() * 50000) }, tags: ['teste', 'validação', tipoAtaque.tipo], atualizadoEm: agora - i * 60000 }); eventosIds.push(eventoId); // Ajustar reputação do IP const delta = SEVERIDADE_SCORE[tipoAtaque.severidade] * -10; await ajustarReputacao( ctx, ip, 'ip', delta, tipoAtaque.severidade, tipoAtaque.severidade === 'critico' || tipoAtaque.severidade === 'alto' ? { blacklist: true } : undefined ); } return { eventosCriados: eventosIds.length, eventosIds }; } }); export const monitorarLogsLogin = internalMutation({ args: {}, returns: v.object({ ataquesDetectados: v.number(), ipsBloqueados: v.number() }), handler: async (ctx) => { // Duplicar lógica de detecção aqui para evitar chamada circular // Sensibilidade maior em dev const janelaMinutos = 10; const dataLimite = Date.now() - janelaMinutos * 60 * 1000; // Buscar todas as tentativas falhas na janela const tentativasFalhas = await ctx.db .query('logsLogin') .withIndex('by_sucesso', (q) => q.eq('sucesso', false)) .filter((q) => q.gte(q.field('timestamp'), dataLimite)) .collect(); // Agrupar por IP para detectar padrões const tentativasPorIP: Record = {}; tentativasFalhas.forEach((t) => { if (t.ipAddress) { tentativasPorIP[t.ipAddress] = (tentativasPorIP[t.ipAddress] || 0) + 1; } }); // Detectar IPs suspeitos (3+ tentativas falhas) const ipsSuspeitos = Object.entries(tentativasPorIP) .filter(([, count]) => count >= 3) .map(([ip, count]) => ({ ip, count })); if (ipsSuspeitos.length === 0) { return { ataquesDetectados: 0, ipsBloqueados: 0 }; } // 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 referencia = `brute_force_${ip}_${Date.now()}`; const agora = Date.now(); await ctx.db.insert('securityEvents', { referencia, timestamp: agora, tipoAtaque: 'brute_force', severidade, status: statusInicial(severidade), descricao: `Ataque de brute force detectado: ${count} tentativas de login falhas do IP ${ip} em ${janelaMinutos} minutos`, origemIp: ip, protocolo: 'http', transporte: 'tcp', detectadoPor: 'monitor_logs_login', metricas: { rpm: Math.round((count / janelaMinutos) * 60) }, tags: ['detecção_automática', 'brute_force', 'login'], atualizadoEm: agora }); // Ajustar reputação do IP const delta = count * -5; await ajustarReputacao( ctx, ip, 'ip', delta, severidade, severidade === 'alto' ? { blacklist: true, bloqueadoAte: agora + 60 * 60 * 1000 } : undefined ); if (severidade === 'alto') { ipsBloqueados++; } } return { ataquesDetectados: ipsSuspeitos.length, ipsBloqueados }; } }); export const deletarConfigRateLimit = mutation({ args: { configId: v.id('rateLimitConfig') }, returns: v.null(), handler: async (ctx, args) => { await assertAdmin(ctx); const config = await ctx.db.get(args.configId); if (!config) { throw new Error('Configuração de rate limit não encontrada'); } await ctx.db.delete(args.configId); return null; } }); // Semeia configurações de rate limit para ambiente de desenvolvimento export const seedRateLimitDev = mutation({ args: {}, returns: v.object({ criadosOuAtualizados: v.number() }), handler: async (ctx) => { assertDevOnly(); let count = 0; // Em dev, usar qualquer usuário existente para auditoria const algumUsuario = await ctx.db.query('usuarios').order('asc').take(1); if (algumUsuario.length === 0) { throw new Error('Seed de rate limit: nenhum usuário encontrado para auditoria (criadoPor).'); } const usuarioId = algumUsuario[0]._id; // Utilitário para upsert por (tipo, identificador) async function upsertConfig(params: { nome: string; tipo: 'ip' | 'usuario' | 'endpoint' | 'global'; identificador?: string; limite: number; janelaSegundos: number; estrategia: 'fixed_window' | 'token_bucket'; acaoExcedido: 'bloquear' | 'throttle' | 'alertar'; prioridade?: number; notas?: string; }) { 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) ) ) .collect(); const agora = Date.now(); if (existing.length > 0) { const doc = existing[0]; await ctx.db.patch(doc._id, { nome: params.nome, limite: params.limite, janelaSegundos: params.janelaSegundos, estrategia: params.estrategia, acaoExcedido: params.acaoExcedido, ativo: true, prioridade: params.prioridade ?? doc.prioridade ?? 0, atualizadoEm: agora, notas: params.notas }); } else { await ctx.db.insert('rateLimitConfig', { nome: params.nome, tipo: params.tipo, identificador: params.identificador ?? (params.tipo === 'global' ? 'global' : undefined), limite: params.limite, janelaSegundos: params.janelaSegundos, estrategia: params.estrategia, acaoExcedido: params.acaoExcedido, ativo: true, prioridade: params.prioridade ?? 0, criadoPor: usuarioId, atualizadoPor: usuarioId, criadoEm: agora, atualizadoEm: agora, notas: params.notas, tags: ['dev', 'seed'] }); } count++; } // Endpoint de login: limite baixo para evidenciar bloqueios await upsertConfig({ nome: 'Bloqueio Login Dev', tipo: 'endpoint', identificador: 'api/auth/sign-in/email', limite: 5, janelaSegundos: 20, estrategia: 'token_bucket', acaoExcedido: 'bloquear', prioridade: 2, notas: 'Dev: evidenciar bloqueio no login' }); // Global: limitar rajadas gerais await upsertConfig({ nome: 'Global Dev', tipo: 'global', limite: 120, janelaSegundos: 60, estrategia: 'fixed_window', acaoExcedido: 'bloquear', prioridade: 1, notas: 'Dev: limitação global' }); // Analisador: manter alta capacidade para não bloquear testes await upsertConfig({ nome: 'Analyzer Alto Throughput', tipo: 'endpoint', identificador: 'http/security/analyze', limite: 1000, janelaSegundos: 10, estrategia: 'token_bucket', acaoExcedido: 'throttle', prioridade: 3, notas: 'Dev: não bloquear analisador' }); return { criadosOuAtualizados: count }; } }); // Remover eventos de teste (tag 'teste') export const limparEventosTeste = mutation({ args: {}, returns: v.object({ removidos: v.number() }), handler: async (ctx) => { assertDevOnly(); const docs = await ctx.db .query('securityEvents') .withIndex('by_timestamp', (q) => q.gte('timestamp', 0)) .order('desc') .take(1000); let removidos = 0; for (const doc of docs) { if (doc.tags && doc.tags.includes('teste')) { await ctx.db.delete(doc._id); removidos++; } } return { removidos }; } }); // Deletar regra de porta export const deletarRegraPorta = mutation({ args: { regraId: v.id('portRules') }, returns: v.null(), handler: async (ctx, args) => { await assertAdmin(ctx); const regra = await ctx.db.get(args.regraId); if (!regra) { throw new Error('Regra de porta não encontrada'); } await ctx.db.delete(args.regraId); return null; } }); // ============================================ // CONFIGURAÇÃO DE BLOQUEIO AUTOMÁTICO // ============================================ // Criar configuração de bloqueio automático export const criarAutoBlockConfig = mutation({ args: { tipoAtaque: ataqueValidator, bloquearAutomatico: v.boolean(), severidadeMinima: severidadeValidator, duracaoBloqueioSegundos: v.optional(v.number()), ativo: v.boolean() }, returns: v.id('autoBlockConfig'), handler: async (ctx, args) => { const usuarioAtual = await assertAdmin(ctx); const agora = Date.now(); // Verificar se já existe configuração para este tipo de ataque const existente = await ctx.db .query('autoBlockConfig') .withIndex('by_tipo', (q) => q.eq('tipoAtaque', args.tipoAtaque)) .first(); if (existente) { throw new Error(`Já existe uma configuração para o tipo de ataque ${args.tipoAtaque}`); } // Validação: não permitir bloqueio permanente para severidade baixa if ( args.bloquearAutomatico && !args.duracaoBloqueioSegundos && (args.severidadeMinima === 'informativo' || args.severidadeMinima === 'baixo') ) { throw new Error( 'Bloqueio permanente só é permitido para severidade moderada, alta ou crítica' ); } const configId = await ctx.db.insert('autoBlockConfig', { tipoAtaque: args.tipoAtaque, bloquearAutomatico: args.bloquearAutomatico, severidadeMinima: args.severidadeMinima, duracaoBloqueioSegundos: args.duracaoBloqueioSegundos, ativo: args.ativo, criadoPor: usuarioAtual._id, atualizadoPor: undefined, criadoEm: agora, atualizadoEm: agora }); return configId; } }); // Atualizar configuração de bloqueio automático export const atualizarAutoBlockConfig = mutation({ args: { configId: v.id('autoBlockConfig'), bloquearAutomatico: v.optional(v.boolean()), severidadeMinima: v.optional(severidadeValidator), duracaoBloqueioSegundos: v.optional(v.number()), ativo: v.optional(v.boolean()) }, returns: v.id('autoBlockConfig'), handler: async (ctx, args) => { const usuarioAtual = await assertAdmin(ctx); const config = await ctx.db.get(args.configId); if (!config) { throw new Error('Configuração não encontrada'); } // Validação: não permitir bloqueio permanente para severidade baixa const severidadeMinima = args.severidadeMinima ?? config.severidadeMinima; const bloquearAutomatico = args.bloquearAutomatico ?? config.bloquearAutomatico; const duracaoBloqueioSegundos = args.duracaoBloqueioSegundos !== undefined ? args.duracaoBloqueioSegundos : config.duracaoBloqueioSegundos; if ( bloquearAutomatico && !duracaoBloqueioSegundos && (severidadeMinima === 'informativo' || severidadeMinima === 'baixo') ) { throw new Error( 'Bloqueio permanente só é permitido para severidade moderada, alta ou crítica' ); } await ctx.db.patch(args.configId, { bloquearAutomatico: args.bloquearAutomatico ?? config.bloquearAutomatico, severidadeMinima: severidadeMinima, duracaoBloqueioSegundos: duracaoBloqueioSegundos, ativo: args.ativo ?? config.ativo, atualizadoPor: usuarioAtual._id, atualizadoEm: Date.now() }); return args.configId; } }); // Deletar configuração de bloqueio automático export const deletarAutoBlockConfig = mutation({ args: { configId: v.id('autoBlockConfig') }, returns: v.null(), handler: async (ctx, args) => { await assertAdmin(ctx); const config = await ctx.db.get(args.configId); if (!config) { throw new Error('Configuração não encontrada'); } await ctx.db.delete(args.configId); return null; } }); // Listar todas as configurações de bloqueio automático export const listarAutoBlockConfigs = query({ args: { ativo: v.optional(v.boolean()), limit: v.optional(v.number()) }, returns: v.array( v.object({ _id: v.id('autoBlockConfig'), tipoAtaque: ataqueValidator, bloquearAutomatico: v.boolean(), severidadeMinima: severidadeValidator, duracaoBloqueioSegundos: v.optional(v.number()), ativo: v.boolean(), criadoPor: v.id('usuarios'), atualizadoPor: v.optional(v.id('usuarios')), criadoEm: v.number(), atualizadoEm: v.number() }) ), handler: async (ctx, args) => { await assertAdmin(ctx); const queryInitializer = ctx.db.query('autoBlockConfig'); const query = args.ativo !== undefined ? queryInitializer.withIndex('by_ativo', (q) => { const ativoValue: boolean = args.ativo!; return q.eq('ativo', ativoValue); }) : queryInitializer; const configs = await query.order('desc').take(args.limit ?? 100); return configs.map((config) => ({ _id: config._id, tipoAtaque: config.tipoAtaque, bloquearAutomatico: config.bloquearAutomatico, severidadeMinima: config.severidadeMinima, duracaoBloqueioSegundos: config.duracaoBloqueioSegundos, ativo: config.ativo, criadoPor: config.criadoPor, atualizadoPor: config.atualizadoPor, criadoEm: config.criadoEm, atualizadoEm: config.atualizadoEm })); } }); // Obter configuração de bloqueio automático por tipo de ataque export const obterAutoBlockConfig = query({ args: { tipoAtaque: ataqueValidator }, returns: v.union( v.object({ _id: v.id('autoBlockConfig'), tipoAtaque: ataqueValidator, bloquearAutomatico: v.boolean(), severidadeMinima: severidadeValidator, duracaoBloqueioSegundos: v.optional(v.number()), ativo: v.boolean(), criadoPor: v.id('usuarios'), atualizadoPor: v.optional(v.id('usuarios')), criadoEm: v.number(), atualizadoEm: v.number() }), v.null() ), handler: async (ctx, args) => { await assertAdmin(ctx); const config = await ctx.db .query('autoBlockConfig') .withIndex('by_tipo', (q) => q.eq('tipoAtaque', args.tipoAtaque)) .filter((q) => q.eq(q.field('ativo'), true)) .first(); if (!config) { return null; } return { _id: config._id, tipoAtaque: config.tipoAtaque, bloquearAutomatico: config.bloquearAutomatico, severidadeMinima: config.severidadeMinima, duracaoBloqueioSegundos: config.duracaoBloqueioSegundos, ativo: config.ativo, criadoPor: config.criadoPor, atualizadoPor: config.atualizadoPor, criadoEm: config.criadoEm, atualizadoEm: config.atualizadoEm }; } });