Files
sgse-app/packages/backend/convex/security.ts
deyvisonwanderley ea01e2401a feat: enhance security features and backend schema
- Added new cron jobs for automatic IP block expiration and threat intelligence synchronization to improve security management.
- Expanded the backend schema to include various types of cyber attack classifications, security event statuses, and incident action types for better incident tracking and response.
- Introduced new tables for network sensors, IP reputation, port rules, threat intelligence feeds, and security events to enhance the overall security infrastructure.
- Updated API definitions to incorporate new security-related modules, ensuring comprehensive access to security functionalities.
2025-11-15 07:25:01 -03:00

1226 lines
32 KiB
TypeScript

import { v } from 'convex/values';
import {
internalMutation,
mutation,
query
} from './_generated/server';
import { internal } from './_generated/api';
import type { Id } from './_generated/dataModel';
import type {
AtaqueCiberneticoTipo,
SeveridadeSeguranca,
StatusEventoSeguranca
} from './schema';
import type { MutationCtx } from './_generated/server';
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',
'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',
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;
}, {});
const KEYWORDS: Record<AtaqueCiberneticoTipo, RegExp[]> = {
phishing: [/phish/i, /spoof/i, /fake login/i],
malware: [/malware/i, /payload/i, /trojan/i],
ransomware: [/ransom/i, /encrypt/i, /locker/i],
brute_force: [/brute/i, /password guess/i, /login flood/i],
credential_stuffing: [/credential/i, /stuffing/i, /combo/i],
sql_injection: [/select\s+.*from/i, /union\s+select/i, /sql/i],
xss: [/script>/i, /onerror=/i, /xss/i],
man_in_the_middle: [/mitm/i, /man-in-the-middle/i, /ssl strip/i],
ddos: [/ddos/i, /flood/i, /pps/i],
engenharia_social: [/social/i, /engenharia/i],
cve_exploit: [/cve-\d{4}-\d+/i, /exploit/i],
apt: [/apt/i, /persistent/i],
zero_day: [/zero[-\s]?day/i, /unknown exploit/i],
supply_chain: [/supply chain/i, /dependency/i],
fileless_malware: [/fileless/i, /memory-resident/i],
polymorphic_malware: [/polymorphic/i, /mutation/i],
ransomware_lateral: [/lateral/i, /spread/i],
deepfake_phishing: [/deepfake/i, /voice clone/i],
adversarial_ai: [/adversarial/i, /model evasion/i],
side_channel: [/side channel/i, /cache timing/i],
firmware_bootloader: [/firmware/i, /uefi/i, /bootloader/i],
bec: [/bec/i, /business email/i],
botnet: [/botnet/i, /c2/i, /command and control/i],
ot_ics: [/ics/i, /scada/i, /plc/i],
quantum_attack: [/quantum/i, /post-quantum/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('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'));
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();
for (const tipo of ATAQUES_PRIORITARIOS) {
const patterns = KEYWORDS[tipo];
if (patterns && patterns.some((regex) => regex.test(corpus))) {
return tipo;
}
}
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) => {
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) => {
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 };
}
});
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;
}
});
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;
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()} - ${evento.tipoAtaque.replace(/_/g, ' ')}`,
descricao: evento.descricao,
lida: false,
criadaEm: Date.now()
});
}
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;
}
});