2858 lines
77 KiB
TypeScript
2858 lines
77 KiB
TypeScript
import { v } from 'convex/values';
|
|
import { internalMutation, mutation, query } from './_generated/server';
|
|
import { internal, api } from './_generated/api';
|
|
import type { Id } from './_generated/dataModel';
|
|
import type {
|
|
AtaqueCiberneticoTipo,
|
|
SeveridadeSeguranca,
|
|
StatusEventoSeguranca
|
|
} from './tables/security';
|
|
import type { MutationCtx, QueryCtx } from './_generated/server';
|
|
import { RateLimiter, SECOND } from '@convex-dev/rate-limiter';
|
|
import { components } from './_generated/api';
|
|
|
|
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;
|
|
};
|
|
|
|
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<AtaqueCiberneticoTipo> = [
|
|
'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',
|
|
'engenharia_social',
|
|
'cve_exploit',
|
|
'bec',
|
|
'side_channel'
|
|
];
|
|
|
|
const BASE_SEVERIDADE: Record<AtaqueCiberneticoTipo, SeveridadeSeguranca> = {
|
|
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',
|
|
engenharia_social: 'moderado',
|
|
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<SeveridadeSeguranca, number> = {
|
|
informativo: 0,
|
|
baixo: 1,
|
|
moderado: 2,
|
|
alto: 3,
|
|
critico: 4
|
|
};
|
|
|
|
const SCORE_SEVERIDADE = Object.entries(SEVERIDADE_SCORE).reduce<
|
|
Record<number, SeveridadeSeguranca>
|
|
>((acc, [nome, score]) => {
|
|
acc[score] = nome as SeveridadeSeguranca;
|
|
return acc;
|
|
}, {});
|
|
|
|
// Padrões robustos de detecção de ataques
|
|
const KEYWORDS: Record<AtaqueCiberneticoTipo, RegExp[]> = {
|
|
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: [
|
|
/<script[^>]*>/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,
|
|
/<img[^>]*src\s*=\s*[^>]*javascript:/i,
|
|
/<iframe[^>]*>/i,
|
|
/<object[^>]*>/i,
|
|
/<embed[^>]*>/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: [
|
|
/<!ENTITY/i,
|
|
/%SYSTEM/i,
|
|
/%PUBLIC/i,
|
|
/<!DOCTYPE[^>]*\[/i,
|
|
/<!ENTITY\s+\w+\s+SYSTEM/i,
|
|
/<!ENTITY\s+\w+\s+PUBLIC/i,
|
|
/&[a-zA-Z_][a-zA-Z0-9_]*;/i,
|
|
/file:\/\//i,
|
|
/php:\/\//i,
|
|
/expect:\/\//i,
|
|
/XXE/i,
|
|
/xml external entity/i
|
|
],
|
|
man_in_the_middle: [/mitm/i, /man-in-the-middle/i, /ssl strip/i, /tls downgrade/i],
|
|
ddos: [/ddos/i, /flood/i, /pps/i, /distributed denial/i, /traffic flood/i],
|
|
engenharia_social: [/social/i, /engenharia/i, /social engineering/i],
|
|
cve_exploit: [/cve-\d{4}-\d+/i, /exploit/i, /cve/i],
|
|
apt: [/apt/i, /persistent/i, /advanced persistent threat/i],
|
|
zero_day: [/zero[-\s]?day/i, /unknown exploit/i, /0-day/i],
|
|
supply_chain: [/supply chain/i, /dependency/i, /supply-chain/i],
|
|
fileless_malware: [/fileless/i, /memory-resident/i, /memory-only/i],
|
|
polymorphic_malware: [/polymorphic/i, /mutation/i, /self-modifying/i],
|
|
ransomware_lateral: [/lateral/i, /spread/i, /lateral movement/i],
|
|
deepfake_phishing: [/deepfake/i, /voice clone/i, /synthetic media/i],
|
|
adversarial_ai: [/adversarial/i, /model evasion/i, /ai attack/i],
|
|
side_channel: [/side channel/i, /cache timing/i, /side-channel/i],
|
|
firmware_bootloader: [/firmware/i, /uefi/i, /bootloader/i, /bios/i],
|
|
bec: [/bec/i, /business email/i, /business email compromise/i],
|
|
botnet: [/botnet/i, /c2/i, /command and control/i, /command-and-control/i],
|
|
ot_ics: [/ics/i, /scada/i, /plc/i, /industrial control/i],
|
|
quantum_attack: [/quantum/i, /post-quantum/i, /quantum computing/i]
|
|
};
|
|
|
|
const CRITICAS: ReadonlySet<SeveridadeSeguranca> = 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('engenharia_social'),
|
|
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'));
|
|
|
|
// 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) => {
|
|
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<AtaqueCiberneticoTipo> = 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) => {
|
|
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) => {
|
|
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: {
|
|
usuarioId: v.id('usuarios'),
|
|
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) => {
|
|
// Aplicar rate limiting por usuário
|
|
const rateLimitResult = await aplicarRateLimit(
|
|
ctx,
|
|
'usuario',
|
|
args.usuarioId,
|
|
'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: 'engenharia_social',
|
|
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: args.usuarioId,
|
|
atualizadoEm: agora
|
|
}),
|
|
tipo: args.acao === 'forcar_blacklist' ? 'block_ip' : 'custom',
|
|
origem: 'manual',
|
|
status: 'concluido',
|
|
executadoPor: args.usuarioId,
|
|
detalhes: args.comentario,
|
|
resultado: 'Registro inicial',
|
|
relacionadoA: undefined,
|
|
criadoEm: agora,
|
|
atualizadoEm: agora
|
|
});
|
|
|
|
return { reputacaoId, status: 'criado' };
|
|
}
|
|
|
|
const patch: Record<string, unknown> = {
|
|
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: {
|
|
usuarioId: v.id('usuarios'),
|
|
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 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: args.usuarioId,
|
|
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: args.usuarioId,
|
|
atualizadoPor: args.usuarioId,
|
|
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) => {
|
|
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 agora = Date.now();
|
|
const acaoId = await ctx.db.insert('incidentActions', {
|
|
eventoId: args.eventoId,
|
|
tipo: args.tipo,
|
|
origem: args.origem,
|
|
status: args.status ?? 'concluido',
|
|
executadoPor: args.executadoPor,
|
|
detalhes: args.detalhes,
|
|
resultado: args.resultado,
|
|
relacionadoA: args.relacionadoA,
|
|
criadoEm: agora,
|
|
atualizadoEm: agora
|
|
});
|
|
|
|
return { acaoId };
|
|
}
|
|
});
|
|
|
|
export const solicitarRelatorioSeguranca = mutation({
|
|
args: {
|
|
solicitanteId: v.id('usuarios'),
|
|
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 relatorioId = await ctx.db.insert('reportRequests', {
|
|
solicitanteId: args.solicitanteId,
|
|
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) => {
|
|
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) => {
|
|
// 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) => {
|
|
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<Record<SeveridadeSeguranca, number>>(
|
|
(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<Record<string, number>>((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) => {
|
|
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
|
|
criadoPor: v.id('usuarios')
|
|
},
|
|
returns: v.object({ _id: v.id('alertConfigs') }),
|
|
handler: async (ctx, args) => {
|
|
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: args.criadoPor,
|
|
criadoEm: agora,
|
|
atualizadoEm: agora
|
|
});
|
|
return { _id: id };
|
|
}
|
|
});
|
|
|
|
export const deletarAlertConfig = mutation({
|
|
args: { configId: v.id('alertConfigs') },
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
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.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
|
|
.query('roles')
|
|
.withIndex('by_nivel', (q) => q.lte('nivel', 1))
|
|
.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')
|
|
.withIndex('by_nivel', (q) => q.lte('nivel', 1))
|
|
.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: {
|
|
usuarioId: v.id('usuarios'),
|
|
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 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: args.usuarioId,
|
|
atualizadoPor: undefined,
|
|
criadoEm: agora,
|
|
atualizadoEm: agora,
|
|
notas: args.notas,
|
|
tags: args.tags
|
|
});
|
|
|
|
return configId;
|
|
}
|
|
});
|
|
|
|
export const atualizarConfigRateLimit = mutation({
|
|
args: {
|
|
configId: v.id('rateLimitConfig'),
|
|
usuarioId: v.id('usuarios'),
|
|
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 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: args.usuarioId,
|
|
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) => {
|
|
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'))
|
|
}),
|
|
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
|
|
};
|
|
}
|
|
|
|
// Calcular severidade
|
|
const severidade = calcularSeveridade(tipoAtaque, undefined, undefined);
|
|
|
|
// 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 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}`,
|
|
origemIp: args.ipOrigem,
|
|
protocolo,
|
|
transporte: 'tcp',
|
|
detectadoPor: 'analisador_http_automatico',
|
|
fingerprint: args.userAgent
|
|
? {
|
|
userAgent: args.userAgent
|
|
}
|
|
: undefined,
|
|
destinoIp: destinoIp ?? undefined,
|
|
tags: ['detecção_automática', 'http', tipoAtaque],
|
|
atualizadoEm: agora
|
|
});
|
|
|
|
// Ajustar reputação do IP se fornecido
|
|
if (args.ipOrigem) {
|
|
const delta = SEVERIDADE_SCORE[severidade] * -10; // Penalidade baseada na severidade
|
|
await ajustarReputacao(
|
|
ctx,
|
|
args.ipOrigem,
|
|
'ip',
|
|
delta,
|
|
severidade,
|
|
severidade === 'critico' || severidade === 'alto' ? { blacklist: true } : undefined
|
|
);
|
|
}
|
|
|
|
return {
|
|
ataqueDetectado: true,
|
|
tipoAtaque,
|
|
severidade,
|
|
eventoId
|
|
};
|
|
}
|
|
});
|
|
|
|
/**
|
|
* 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<string, number> = {};
|
|
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) => {
|
|
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<string, number> = {};
|
|
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'),
|
|
usuarioId: v.id('usuarios')
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
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) => {
|
|
let count = 0;
|
|
// Obter um usuário existente para campos de 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) => {
|
|
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'),
|
|
usuarioId: v.id('usuarios')
|
|
},
|
|
returns: v.null(),
|
|
handler: async (ctx, args) => {
|
|
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;
|
|
}
|
|
});
|