From 70d405d98d2d4fa552c61a786a5d3cddfb423eed Mon Sep 17 00:00:00 2001 From: deyvisonwanderley Date: Sun, 16 Nov 2025 07:37:36 -0300 Subject: [PATCH] feat: implement alert configuration and recent report features - Added alert configuration management for email and chat notifications, allowing users to set preferences for severity levels, attack types, and notification channels. - Introduced functionality to save, edit, and delete alert configurations, enhancing user control over security notifications. - Implemented a new query to list recent security reports, providing users with quick access to the latest security incidents. - Enhanced the backend schema to support alert configurations and recent report tracking, improving overall security management capabilities. --- .../components/ti/CybersecurityWizcard.svelte | 456 +++++++++++++++++- packages/backend/convex/schema.ts | 17 + packages/backend/convex/security.ts | 159 +++++- scripts/ativar_venv.sh | 1 + scripts/teste_seguranca.py | 54 ++- 5 files changed, 650 insertions(+), 37 deletions(-) diff --git a/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte b/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte index 3dee42b..313e836 100644 --- a/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte +++ b/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte @@ -16,7 +16,117 @@ limit: 50 }); const meuPerfil = useQuery(api.usuarios.obterPerfil, {}); + const usuariosQuery = useQuery(api.usuarios.listar, {}); + const usuariosParaChat = useQuery(api.usuarios.listarParaChat, {}); + const relatoriosRecentes = useQuery(api.security.listarRelatoriosRecentes, { limit: 10 }); + const health = useQuery(api.security.healthStatus, {}); + const alertConfigs = useQuery(api.security.listarAlertConfigs, { limit: 100 }); + // Estado local para preferências de alertas + let alertEmails = $state(''); + let alertSeveridadeMin: SeveridadeSeguranca = 'alto'; + let alertTiposAtaque: string[] = []; + let alertReenvioMin = $state(15); + let alertTemplate = $state('incidente_critico'); + let alertUsersChat = $state(''); + let chatSeveridadeMin: SeveridadeSeguranca = 'alto'; + let chatTiposAtaque: string[] = []; + let chatReenvioMin = $state(10); + let enviarPorEmail = $state(true); + let enviarPorChat = $state(false); + let editarAlertConfigId: Id<'alertConfigs'> | null = null; + + // Sugestões a partir de configs salvas + const sugestoesEmails = $derived.by(() => { + const set = new Set(); + for (const cfg of alertConfigs?.data ?? []) { + for (const e of cfg.emails ?? []) set.add(e); + } + for (const u of usuariosQuery?.data ?? []) { + if (u?.email) set.add(u.email); + } + return Array.from(set).slice(0, 16); + }); + const sugestoesChatUsers = $derived.by(() => { + const set = new Set(); + for (const cfg of alertConfigs?.data ?? []) { + for (const u of cfg.chatUsers ?? []) set.add(u); + } + for (const u of usuariosQuery?.data ?? []) { + if (u?.username) set.add(u.username); + } + return Array.from(set).slice(0, 16); + }); + function adicionarEmailSugestao(email: string) { + const linhas = alertEmails.split('\n').map((s) => s.trim()).filter(Boolean); + if (!linhas.includes(email)) { + linhas.push(email); + alertEmails = linhas.join('\n'); + } + } + function adicionarChatSugestao(user: string) { + const linhas = alertUsersChat.split('\n').map((s) => s.trim()).filter(Boolean); + if (!linhas.includes(user)) { + linhas.push(user); + alertUsersChat = linhas.join('\n'); + } + } + + function parseLinhasParaArray(valor: string): string[] { + return valor + .split('\n') + .map((s) => s.trim()) + .filter((s) => s.length > 0); + } + async function salvarPreferenciasAlertas(configId?: string) { + try { + const canais = { email: enviarPorEmail, chat: enviarPorChat }; + const resp = await client.mutation(api.security.salvarAlertConfig, { + configId: configId as Id<'alertConfigs'> | undefined, + nome: alertTemplate || 'Notificação', + canais, + emails: parseLinhasParaArray(alertEmails), + chatUsers: parseLinhasParaArray(alertUsersChat), + severidadeMin: alertSeveridadeMin, + tiposAtaque: (alertTiposAtaque as AtaqueCiberneticoTipo[]), + reenvioMin: alertReenvioMin, + criadoPor: obterUsuarioId() + }); + feedback = { tipo: 'success', mensagem: 'Preferências salvas.' }; + editarAlertConfigId = resp._id as Id<'alertConfigs'>; + return resp._id; + } catch (erro: unknown) { + feedback = { tipo: 'error', mensagem: mensagemErro(erro) }; + } + } + async function excluirPreferenciasAlertas(configId: Id<'alertConfigs'>) { + try { + await client.mutation(api.security.deletarAlertConfig, { configId }); + feedback = { tipo: 'success', mensagem: 'Configuração removida.' }; + } catch (erro: unknown) { + feedback = { tipo: 'error', mensagem: mensagemErro(erro) }; + } + } + function carregarParaEdicao(config: { + _id: Id<'alertConfigs'>; + nome: string; + canais: { email: boolean; chat: boolean }; + emails: string[]; + chatUsers: string[]; + severidadeMin: SeveridadeSeguranca; + tiposAtaque?: AtaqueCiberneticoTipo[]; + reenvioMin: number; + }) { + alertTemplate = config.nome ?? 'incidente_critico'; + enviarPorEmail = config.canais?.email ?? true; + enviarPorChat = config.canais?.chat ?? false; + alertEmails = (config.emails ?? []).join('\n'); + alertUsersChat = (config.chatUsers ?? []).join('\n'); + alertSeveridadeMin = config.severidadeMin ?? 'alto'; + alertTiposAtaque = (config.tiposAtaque ?? []) as string[]; + alertReenvioMin = config.reenvioMin ?? 15; + editarAlertConfigId = config._id; + } const severidadesDisponiveis: SeveridadeSeguranca[] = [ 'informativo', 'baixo', @@ -88,7 +198,8 @@ let alertaVisualAtivo = $state(true); // Contagem de novos eventos detectados em tempo real (sem recarregar) let novosEventos = $state(0); - let ultimoTotalEventos: number | null = null; + // Timestamp do último evento observado no feed para cálculo de "novos" + let ultimoTsVisto = $state(Date.now()); let ipManual = $state(''); let comentarioManual = $state(''); let porta = $state(443); @@ -146,26 +257,71 @@ // Efeito: observar chegada de novos eventos e acionar toast/contador $effect(() => { - const atual = (eventos?.data ?? []).length; - if (ultimoTotalEventos === null) { - ultimoTotalEventos = atual; - return; - } - if (atual > ultimoTotalEventos) { - const delta = atual - ultimoTotalEventos; - novosEventos += delta; - feedback = { - tipo: 'success', - mensagem: `🔔 ${delta} novo(s) evento(s) de segurança detectado(s) em tempo real` - }; - // Opcional: destacar visualmente + const lista = (eventos?.data ?? []); + if (!lista.length) return; + // conta apenas eventos com timestamp maior que o último visto + const novos = lista.filter((e) => e.timestamp > ultimoTsVisto).length; + if (novos > 0) { + novosEventos += novos; + ultimoTsVisto = Math.max(ultimoTsVisto, ...lista.map((e) => e.timestamp)); if (alertaVisualAtivo) { - // classe CSS já existente de alertas visuais; aqui mantemos apenas o toast + feedback = { + tipo: 'success', + mensagem: `🔔 ${novos} novo(s) evento(s) de segurança detectado(s)` + }; } } - ultimoTotalEventos = atual; }); + // Dados para gráfico “Realtime por tipo” (últimos 60 min em buckets de 5 min) + const janelaMs = 60 * 60 * 1000; + const bucketMs = 5 * 60 * 1000; + const coresTipo: Record = { + ddos: 'rgba(248,113,113,0.9)', + sql_injection: 'rgba(59,130,246,0.9)', + xss: 'rgba(234,179,8,0.9)', + brute_force: 'rgba(99,102,241,0.9)', + path_traversal: 'rgba(34,197,94,0.9)', + command_injection: 'rgba(236,72,153,0.9)', + nosql_injection: 'rgba(20,184,166,0.9)' + }; + const tiposParaChart = Object.keys(coresTipo); + const timelineBuckets = $derived.by(() => { + const agora = Date.now(); + const inicio = agora - janelaMs; + const lista = (eventos?.data ?? []).filter((e) => e.timestamp >= inicio); + const numBuckets = Math.floor(janelaMs / bucketMs); + const buckets: Array<{ + inicio: number; + fim: number; + counts: Record; + topDestinos: Record; + }> = []; + for (let i = 0; i < numBuckets; i++) { + const bIni = inicio + i * bucketMs; + const bFim = bIni + bucketMs; + const counts: Record = {}; + const topDestinos: Record = {}; + for (const tipo of tiposParaChart) counts[tipo] = 0; + for (const ev of lista) { + if (ev.timestamp >= bIni && ev.timestamp < bFim) { + const t = ev.tipoAtaque as string; + if (t in counts) counts[t] += 1; + const key = `${ev.destinoIp ?? 'app'}|${ev.protocolo ?? 'http'}`; + topDestinos[key] = (topDestinos[key] ?? 0) + 1; + } + } + buckets.push({ inicio: bIni, fim: bFim, counts, topDestinos }); + } + return buckets; + }); + const maxBucketTotal = $derived.by(() => + Math.max( + 1, + ...timelineBuckets.map((b) => Object.values(b.counts).reduce((a, n) => a + n, 0)) + ) + ); + function maxSeriesValue(dataset: Array>): number { let max = 1; for (const serie of dataset) { @@ -526,6 +682,155 @@ + +
+

Alertas e Notificações

+

Configure destinatários, níveis e tipos de alarme e reenvio.

+
+ +
+

Emails de destino

+ + {#if sugestoesEmails.length} +
+ {#each sugestoesEmails as s (s)} + + {/each} +
+ {/if} +
+ + +
+
+ + +
+ + + +
+ +
+

Alertas por Chat

+ + + {#if sugestoesChatUsers.length} +
+ {#each sugestoesChatUsers as s (s)} + + {/each} +
+ {/if} +
+ + +
+ + +
+
+
+

Configurações salvas

+ {#if alertConfigs?.data?.length} +
+ {#each alertConfigs.data as cfg (cfg._id)} +
+
+
+

{cfg.nome}

+

+ Canais: {cfg.canais.email ? 'Email' : ''}{cfg.canais.email && cfg.canais.chat ? ' + ' : ''}{cfg.canais.chat ? 'Chat' : ''} + • Sev. mínima: {severityLabels[cfg.severidadeMin]} + • Reenvio: {cfg.reenvioMin} min +

+ {#if cfg.emails.length} +

Emails: {cfg.emails.join(', ')}

+ {/if} + {#if cfg.chatUsers.length} +

Chat: {cfg.chatUsers.join(', ')}

+ {/if} +
+
+ + +
+
+
+ {/each} +
+ {:else} +

Nenhuma configuração salva.

+ {/if} +
+
@@ -535,6 +840,12 @@ Correlação temporal entre DDoS, SQLi, ataques avançados e bloqueios automáticos.

+ {#if health?.data} +
+ + {health.data.ok ? 'Saúde OK' : 'Instável'} · Pendentes: {health.data.pendingReports} +
+ {/if}
+ +
+

Realtime por tipo (60 min)

+
+ {#each timelineBuckets as b} +
+
+ {#each tiposParaChart as t} + {#if b.counts[t] > 0} +
a + n, 0))) * 100}%; background:${coresTipo[t]}`} + title={`${t}: ${b.counts[t]}`}>
+ {/if} + {/each} +
+
+ {new Date(b.inicio).toLocaleTimeString('pt-BR', { hour12: false, hour: '2-digit', minute: '2-digit' })} +
+
+ {/each} +
+
+ {#each tiposParaChart as t} + {t.replace('_', ' ')} + {/each} +
+ +
+

Top destinos (IP · protocolo)

+ + + + + + + + + + {#each [...new Map( + timelineBuckets + .map((b) => Object.entries(b.topDestinos)) + .flat() + .reduce((acc, [k, v]) => acc.set(k, (acc.get(k) ?? 0) + (v as number)), new Map()) + )].slice(0, 8) as item} + {#key item[0]} + {@const partes = item[0].split('|')} + + + + + + {/key} + {/each} + +
DestinoProtocoloOcorrências
{partes[0]}{partes[1]}{item[1] as number}
+
+
+
{#each severidadesDisponiveis as severidade (severidade)}
-
+
6 ? 'max-h-[28rem] overflow-auto pr-2' : ''}`}> {#if eventosFiltrados.length === 0}

Nenhum evento correspondente aos filtros.

{:else} @@ -815,10 +1184,12 @@

{evento.descricao}

-
- Origem: - {evento.origemIp ?? 'n/d'} -
+ {#if evento.origemIp} +
+ Origem: + {evento.origemIp} +
+ {/if}
Destino: {evento.destinoIp ?? 'n/d'}:{evento.destinoPorta ?? '--'} @@ -883,7 +1254,7 @@

Lista Negra Inteligente

-
    +
      {#if ipCriticos.length === 0}
    • Nenhum IP crítico listado.
    • {:else} @@ -908,9 +1279,10 @@
-
+ +

Regras de Portas Monitoradas

-
+
{#if regras.length === 0}

Nenhuma regra cadastrada.

{:else} @@ -927,7 +1299,7 @@

{#if regra.expiraEm}

- Expira em: {new Date(regra.expiraEm).toLocaleString('pt-BR', { hour12: false })} + Expira em: {new Date(regra.expiraEm).toString().slice(0, 21)}

{/if}
@@ -978,7 +1350,8 @@
-
+ +

Relatórios refinados

+ +
+
Relatórios recentes
+ {#if relatoriosRecentes?.data?.length} +
+ + + + + + + + + + + {#each relatoriosRecentes.data as r (r._id)} + + + + + + + {/each} + +
StatusCriadoConcluídoObservações
+ {r.status} + {new Date(r.criadoEm).toLocaleString('pt-BR', { hour12: false })}{r.concluidoEm ? new Date(r.concluidoEm).toLocaleString('pt-BR', { hour12: false }) : '-'}{r.observacoes ?? '-'}
+
+ {:else} +

Nenhum relatório recente.

+ {/if}
diff --git a/packages/backend/convex/schema.ts b/packages/backend/convex/schema.ts index 6d42231..df28206 100644 --- a/packages/backend/convex/schema.ts +++ b/packages/backend/convex/schema.ts @@ -1300,4 +1300,21 @@ export default defineSchema({ .index("by_tipo_identificador", ["tipo", "identificador"]) .index("by_ativo", ["ativo"]) .index("by_prioridade", ["prioridade"]) + , + alertConfigs: defineTable({ + nome: v.string(), + canais: v.object({ + email: v.boolean(), + chat: v.boolean(), + }), + emails: v.array(v.string()), + chatUsers: v.array(v.string()), + severidadeMin: severidadeSeguranca, + tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)), + reenvioMin: v.number(), + criadoPor: v.id("usuarios"), + criadoEm: v.number(), + atualizadoEm: v.number(), + }) + .index("by_criadoEm", ["criadoEm"]) }); diff --git a/packages/backend/convex/security.ts b/packages/backend/convex/security.ts index c57c15a..26d1852 100644 --- a/packages/backend/convex/security.ts +++ b/packages/backend/convex/security.ts @@ -1244,6 +1244,63 @@ export const solicitarRelatorioSeguranca = mutation({ } }); +// 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 + }; + } +}); export const processarRelatorioSegurancaInternal = internalMutation({ args: { relatorioId: v.id('reportRequests') @@ -1319,6 +1376,97 @@ export const processarRelatorioSegurancaInternal = internalMutation({ } }); +// ---------- 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(), + 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, + 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(), + 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, + 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, + 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') @@ -1729,6 +1877,12 @@ export const analisarRequisicaoHTTP = mutation({ // 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(); @@ -1740,12 +1894,13 @@ export const analisarRequisicaoHTTP = mutation({ status: statusInicial(severidade), descricao: `Ataque ${tipoAtaque} detectado na requisição HTTP ${args.method} ${args.url}`, origemIp: args.ipOrigem, - protocolo: 'http', + 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 }); @@ -2166,7 +2321,7 @@ export const seedRateLimitDev = mutation({ nome: 'Bloqueio Login Dev', tipo: 'endpoint', identificador: 'api/auth/sign-in/email', - limite: 10, + limite: 5, janelaSegundos: 20, estrategia: 'token_bucket', acaoExcedido: 'bloquear', diff --git a/scripts/ativar_venv.sh b/scripts/ativar_venv.sh index e0d30d1..746963e 100755 --- a/scripts/ativar_venv.sh +++ b/scripts/ativar_venv.sh @@ -23,3 +23,4 @@ fi + diff --git a/scripts/teste_seguranca.py b/scripts/teste_seguranca.py index 26179cc..56914ff 100755 --- a/scripts/teste_seguranca.py +++ b/scripts/teste_seguranca.py @@ -78,6 +78,7 @@ class SegurancaTeste: endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze") bloqueado = False + ip_origem = f"203.0.113.{random.randint(10, 250)}" for i, senha in enumerate(senhas_comuns[:tentativas], 1): try: payload = { @@ -88,6 +89,7 @@ class SegurancaTeste: response = self.session.post( endpoint, json=payload, + headers={"X-Forwarded-For": ip_origem}, timeout=5, allow_redirects=False ) @@ -134,7 +136,15 @@ class SegurancaTeste: # Registrar tentativa de brute force no analisador para validar detecção no backend try: mark = "multiple failed login; brute force password guess" - r2 = self.session.post(endpoint_analyze, data=mark, headers={"Content-Type":"text/plain","X-Test-Scenario":"brute_force"}) + r2 = self.session.post( + endpoint_analyze, + data=mark, + headers={ + "Content-Type": "text/plain", + "X-Test-Scenario": "brute_force", + "X-Forwarded-For": ip_origem + } + ) if r2.status_code == 200: jd = r2.json() if jd.get("ataqueDetectado") and jd.get("tipoAtaque") == "brute_force": @@ -174,15 +184,17 @@ class SegurancaTeste: ] endpoint_login = f"{self.base_url}/api/auth/sign-in/email" - endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze") + endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze?dst=127.0.0.1&proto=http") detectado = False for payload in payloads_sql: + ip_origem = f"203.0.113.{random.randint(10, 250)}" try: # Teste no campo email response = self.session.post( endpoint_login, json={"email": payload, "password": "test"}, + headers={"X-Forwarded-For": ip_origem}, timeout=5, allow_redirects=False ) @@ -207,7 +219,11 @@ class SegurancaTeste: time.sleep(0.3) # Registrar via analisador HTTP para validar detecção no backend try: - r2 = self.session.post(endpoint_analyze, data=payload, headers={"Content-Type":"text/plain"}) + r2 = self.session.post( + endpoint_analyze, + data=payload, + headers={"Content-Type": "text/plain", "X-Forwarded-For": ip_origem} + ) if r2.status_code == 200: jd = r2.json() if jd.get("ataqueDetectado") and jd.get("tipoAtaque") == "sql_injection": @@ -257,10 +273,12 @@ class SegurancaTeste: for payload in payloads_xss: try: + ip_origem = f"203.0.113.{random.randint(100, 200)}" # Teste no campo email response = self.session.post( endpoint_login, json={"email": payload, "password": "test"}, + headers={"X-Forwarded-For": ip_origem}, timeout=5, allow_redirects=False ) @@ -284,7 +302,7 @@ class SegurancaTeste: time.sleep(0.3) # Registrar via analisador HTTP try: - r2 = self.session.post(endpoint_analyze, data=payload, headers={"Content-Type":"text/html"}) + r2 = self.session.post(endpoint_analyze, data=payload, headers={"Content-Type":"text/html","X-Forwarded-For": ip_origem}) if r2.status_code == 200: jd = r2.json() if jd.get("ataqueDetectado") and jd.get("tipoAtaque") == "xss": @@ -414,15 +432,17 @@ class SegurancaTeste: ] endpoint = f"{self.base_url}/api/auth/sign-in/email" - endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze") + endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze?dst=127.0.0.1&proto=http") detectado = False for payload in payloads_path: try: + ip_origem = f"203.0.113.{random.randint(10, 250)}" # Tentar em diferentes campos response = self.session.post( endpoint, json={"email": payload, "password": "test"}, + headers={"X-Forwarded-For": ip_origem}, timeout=5, allow_redirects=False ) @@ -437,7 +457,7 @@ class SegurancaTeste: time.sleep(0.3) # Registrar via analisador HTTP try: - r2 = self.session.post(endpoint_analyze + f"?file={payload}") + r2 = self.session.post(endpoint_analyze + f"&file={payload}", headers={"X-Forwarded-For": ip_origem}) if r2.status_code == 200: jd = r2.json() if jd.get("ataqueDetectado") and jd.get("tipoAtaque") == "path_traversal": @@ -476,14 +496,16 @@ class SegurancaTeste: ] endpoint = f"{self.base_url}/api/auth/sign-in/email" - endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze") + endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze?dst=127.0.0.1&proto=http") detectado = False for payload in payloads_cmd: try: + ip_origem = f"203.0.113.{random.randint(10, 250)}" response = self.session.post( endpoint, json={"email": f"test{payload}@example.com", "password": "test"}, + headers={"X-Forwarded-For": ip_origem}, timeout=5, allow_redirects=False ) @@ -498,7 +520,7 @@ class SegurancaTeste: time.sleep(0.3) # Registrar via analisador HTTP try: - r2 = self.session.post(endpoint_analyze, data=payload, headers={"Content-Type":"text/plain"}) + r2 = self.session.post(endpoint_analyze, data=payload, headers={"Content-Type":"text/plain","X-Forwarded-For": ip_origem}) if r2.status_code == 200: jd = r2.json() if jd.get("ataqueDetectado") and jd.get("tipoAtaque") == "command_injection": @@ -538,6 +560,7 @@ class SegurancaTeste: response = self.session.post( endpoint, json={"email": payload, "password": {"$ne": None}}, + headers={"X-Forwarded-For": f"203.0.113.{random.randint(10, 250)}"}, timeout=5, allow_redirects=False ) @@ -574,7 +597,8 @@ class SegurancaTeste: for payload in payloads_xxe: try: # Tentar enviar como XML - headers = {'Content-Type': 'application/xml'} + ip_origem = f"203.0.113.{random.randint(10, 250)}" + headers = {'Content-Type': 'application/xml', 'X-Forwarded-For': ip_origem} response = self.session.post( endpoint, data=payload, @@ -591,6 +615,18 @@ class SegurancaTeste: self.resultados['xxe']['detectado'] = True time.sleep(0.3) + # Registrar via analisador HTTP + try: + endpoint_analyze = urljoin(self.convex_url if self.convex_url.endswith('/') else self.convex_url + '/', "http/security/analyze?dst=127.0.0.1&proto=http") + r2 = self.session.post(endpoint_analyze, data=payload, headers={'Content-Type': 'application/xml', 'X-Forwarded-For': ip_origem}) + if r2.status_code == 200: + jd = r2.json() + if jd.get("ataqueDetectado") and jd.get("tipoAtaque") == "xxe": + self.log("XXE", "✅ DETECTADO (analisador)!", Colors.OKGREEN) + detectado = True + self.resultados['xxe']['detectado'] = True + except Exception: + pass except requests.exceptions.RequestException as e: self.log("XXE", f"Erro: {str(e)}", Colors.WARNING)