diff --git a/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte b/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte index ecfd7fb..6d20698 100644 --- a/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte +++ b/apps/web/src/lib/components/ti/CybersecurityWizcard.svelte @@ -5,6 +5,8 @@ import { api } from '@sgse-app/backend/convex/_generated/api'; import type { AtaqueCiberneticoTipo, SeveridadeSeguranca } from '@sgse-app/backend/convex/schema'; import { authStore } from '$lib/stores/auth.svelte'; + import jsPDF from 'jspdf'; + import autoTable from 'jspdf-autotable'; const client = useConvexClient(); const visaoCamadas = useQuery(api.security.obterVisaoCamadas, { periodoHoras: 6, buckets: 28 }); @@ -507,8 +509,8 @@ const data = new Date(valor); return Number.isNaN(data.getTime()) ? Date.now() : data.getTime(); } - // Imprimir conteúdo do relatório (usa campo observações JSON estruturado) - function imprimirRelatorio(r: { + // Gerar PDF do relatório com detalhes completos + async function imprimirRelatorio(r: { _id: Id<'reportRequests'>; status: 'pendente' | 'processando' | 'concluido' | 'falhou'; criadoEm: number; @@ -516,80 +518,361 @@ observacoes?: string; }) { if (typeof window === 'undefined') return; - const win = window.open('', '_blank', 'noopener,noreferrer,width=900,height=700'); - if (!win) return; - let conteudo: string; + try { - const data = r.observacoes ? (JSON.parse(r.observacoes) as any) : null; - const total = data?.total ?? '—'; - const porSeveridade = data?.porSeveridade ?? {}; - const porAtaque = data?.porAtaque ?? {}; - const linhasSev = Object.entries(porSeveridade) - .map(([k, v]) => `${k}${v as number}`) - .join(''); - const linhasAtk = Object.entries(porAtaque) - .map(([k, v]) => `${k}${v as number}`) - .join(''); + // Buscar eventos de segurança para incluir detalhes + const eventosDetalhados = await client.query(api.security.listarEventosSeguranca, { + limit: 500 + }); + + // Buscar IPs bloqueados + const ipsBloqueados = await client.query(api.security.listarReputacoes, { + limit: 100, + lista: 'blacklist' + }); + + const doc = new jsPDF(); + let yPosition = 20; + + // Cabeçalho + doc.setFontSize(20); + doc.setFont('helvetica', 'bold'); + doc.text('Relatório de Segurança Detalhado', 105, yPosition, { align: 'center' }); + yPosition += 10; + + // Informações do relatório + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); const criadoStr = new Date(r.criadoEm).toLocaleString('pt-BR', { hour12: false }); const concluidoStr = r.concluidoEm ? new Date(r.concluidoEm).toLocaleString('pt-BR', { hour12: false }) : '—'; const agoraStr = new Date().toLocaleString('pt-BR', { hour12: false }); - conteudo = - '' + - 'Relatório de Segurança' + - '' + - '
' + - '

Relatório de Segurança

' + - '
Status: ' + - r.status + - ' · Criado: ' + - criadoStr + - ' · Concluído: ' + - concluidoStr + - '
' + - '

Resumo

' + - '

Total de eventos no período: ' + - total + - '

' + - '

Por Severidade

' + - (linhasSev || '') + - '
SeveridadeQtde

Por Tipo de Ataque

' + - (linhasAtk || '') + - '
TipoQtde
' + - '' + - ''; - } catch { - const obs = (r.observacoes ?? '').replace(/'; - const scriptClose = ''; - conteudo = - '' + - 'Relatório' + - '
' +
-				obs +
-				'
' + - scriptOpen + - 'window.print()' + - scriptClose; + + doc.setFontSize(9); + doc.setTextColor(100, 100, 100); + doc.text(`Status: ${r.status}`, 20, yPosition); + doc.text(`Criado: ${criadoStr}`, 105, yPosition, { align: 'center' }); + doc.text(`Concluído: ${concluidoStr}`, 190, yPosition, { align: 'right' }); + yPosition += 10; + + // Parse dos dados + type RelatorioData = { + total?: number | string; + porSeveridade?: Record; + porAtaque?: Record; + incluiuIndicadores?: boolean; + incluiuMetricas?: boolean; + incluiuAcoes?: boolean; + }; + let data: RelatorioData | null = null; + try { + data = r.observacoes ? (JSON.parse(r.observacoes) as RelatorioData) : null; + } catch { + // Se não conseguir parsear, exibe como texto simples + doc.setFontSize(12); + doc.setTextColor(0, 0, 0); + doc.text('Conteúdo do Relatório:', 20, yPosition); + yPosition += 8; + doc.setFontSize(9); + const lines = doc.splitTextToSize(r.observacoes ?? 'Sem observações', 170); + doc.text(lines, 20, yPosition); + doc.save(`Relatorio_Seguranca_${new Date().getTime()}.pdf`); + return; + } + + // Resumo Executivo + doc.setFontSize(14); + doc.setTextColor(0, 0, 0); + doc.setFont('helvetica', 'bold'); + doc.text('Resumo Executivo', 20, yPosition); + yPosition += 10; + + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + const total = data?.total ?? '—'; + doc.text(`Total de eventos no período: ${total}`, 20, yPosition); + yPosition += 7; + doc.text(`IPs bloqueados: ${ipsBloqueados?.length ?? 0}`, 20, yPosition); + yPosition += 7; + doc.text(`Eventos detalhados analisados: ${eventosDetalhados?.length ?? 0}`, 20, yPosition); + yPosition += 12; + + // Tabela por Severidade + const porSeveridade = data?.porSeveridade ?? {}; + const dadosSeveridade = Object.entries(porSeveridade).map(([k, v]) => [ + k.charAt(0).toUpperCase() + k.slice(1), + String(v as number) + ]); + + if (dadosSeveridade.length > 0) { + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text('Distribuição por Severidade', 20, yPosition); + yPosition += 8; + + autoTable(doc, { + startY: yPosition, + head: [['Severidade', 'Quantidade', '% do Total']], + body: dadosSeveridade.map(([sev, qtd]) => { + const totalNum = typeof total === 'number' ? total : parseInt(String(total)) || 1; + const percentual = ((parseInt(String(qtd)) / totalNum) * 100).toFixed(1); + return [sev, String(qtd), `${percentual}%`]; + }), + theme: 'grid', + headStyles: { fillColor: [41, 128, 185], fontStyle: 'bold', textColor: [255, 255, 255] }, + styles: { fontSize: 9 }, + margin: { left: 20, right: 20 } + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + yPosition = (doc as any).lastAutoTable.finalY + 10; + } + + // Nova página se necessário + if (yPosition > 250) { + doc.addPage(); + yPosition = 20; + } + + // Tabela por Tipo de Ataque + const porAtaque = data?.porAtaque ?? {}; + const dadosAtaque = Object.entries(porAtaque) + .sort(([, a], [, b]) => (b as number) - (a as number)) + .map(([k, v]) => [k, String(v as number)]); + + if (dadosAtaque.length > 0) { + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text('Distribuição por Tipo de Ataque', 20, yPosition); + yPosition += 8; + + const totalNum = typeof total === 'number' ? total : parseInt(String(total)) || 1; + autoTable(doc, { + startY: yPosition, + head: [['Tipo de Ataque', 'Quantidade', '% do Total']], + body: dadosAtaque.map(([tipo, qtd]) => { + const percentual = ((parseInt(String(qtd)) / totalNum) * 100).toFixed(1); + return [tipo, String(qtd), `${percentual}%`]; + }), + theme: 'grid', + headStyles: { fillColor: [192, 57, 43], fontStyle: 'bold', textColor: [255, 255, 255] }, + styles: { fontSize: 9 }, + margin: { left: 20, right: 20 } + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + yPosition = (doc as any).lastAutoTable.finalY + 10; + } + + // Nova página se necessário + if (yPosition > 240) { + doc.addPage(); + yPosition = 20; + } + + // IPs Bloqueados + if (ipsBloqueados && ipsBloqueados.length > 0) { + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text('IPs Bloqueados (Blacklist)', 20, yPosition); + yPosition += 8; + + const ipsData = ipsBloqueados + .slice(0, 50) + .map((ip) => [ + ip.indicador ?? 'N/A', + String(ip.reputacao ?? 'N/A'), + ip.ultimoRegistro + ? new Date(ip.ultimoRegistro).toLocaleString('pt-BR', { hour12: false }) + : 'N/A' + ]); + + autoTable(doc, { + startY: yPosition, + head: [['Endereço IP', 'Reputação', 'Última Atualização']], + body: ipsData, + theme: 'grid', + headStyles: { fillColor: [142, 68, 173], fontStyle: 'bold', textColor: [255, 255, 255] }, + styles: { fontSize: 8 }, + margin: { left: 20, right: 20 } + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + yPosition = (doc as any).lastAutoTable.finalY + 10; + } + + // Nova página se necessário + if (yPosition > 240) { + doc.addPage(); + yPosition = 20; + } + + // Eventos Detalhados + if (eventosDetalhados && eventosDetalhados.length > 0) { + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text('Eventos de Segurança Detalhados', 20, yPosition); + yPosition += 8; + + // Limitar a 100 eventos mais recentes para não sobrecarregar o PDF + const eventosOrdenados = eventosDetalhados + .sort((a, b) => b.timestamp - a.timestamp) + .slice(0, 100); + + const eventosData = eventosOrdenados.map((evento) => [ + new Date(evento.timestamp).toLocaleString('pt-BR', { hour12: false }), + evento.tipoAtaque, + evento.severidade, + evento.status, + evento.origemIp ?? 'N/A', + evento.destinoIp ?? 'N/A', + evento.destinoPorta ? String(evento.destinoPorta) : 'N/A', + evento.protocolo ?? 'N/A', + evento.descricao.length > 50 + ? evento.descricao.substring(0, 47) + '...' + : evento.descricao + ]); + + autoTable(doc, { + startY: yPosition, + head: [ + [ + 'Data/Hora', + 'Tipo', + 'Severidade', + 'Status', + 'IP Origem', + 'IP Destino', + 'Porta', + 'Protocolo', + 'Descrição' + ] + ], + body: eventosData, + theme: 'grid', + headStyles: { + fillColor: [39, 174, 96], + fontStyle: 'bold', + textColor: [255, 255, 255], + fontSize: 7 + }, + styles: { fontSize: 7, cellPadding: 2 }, + margin: { left: 10, right: 10 }, + columnStyles: { + 0: { cellWidth: 30 }, + 1: { cellWidth: 25 }, + 2: { cellWidth: 20 }, + 3: { cellWidth: 20 }, + 4: { cellWidth: 25 }, + 5: { cellWidth: 25 }, + 6: { cellWidth: 15 }, + 7: { cellWidth: 15 }, + 8: { cellWidth: 40 } + } + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + yPosition = (doc as any).lastAutoTable.finalY + 10; + + if (eventosDetalhados.length > 100) { + doc.setFontSize(8); + doc.setTextColor(100, 100, 100); + doc.text( + `Nota: Exibindo apenas os 100 eventos mais recentes de um total de ${eventosDetalhados.length} eventos.`, + 20, + yPosition + ); + yPosition += 8; + } + } + + // Nova página se necessário + if (yPosition > 240) { + doc.addPage(); + yPosition = 20; + } + + // Estatísticas Adicionais + if (eventosDetalhados && eventosDetalhados.length > 0) { + doc.setFontSize(12); + doc.setFont('helvetica', 'bold'); + doc.text('Estatísticas Adicionais', 20, yPosition); + yPosition += 10; + + // IPs únicos + const ipsUnicos = new Set( + eventosDetalhados.filter((e) => e.origemIp).map((e) => e.origemIp) + ).size; + const portasUnicas = new Set( + eventosDetalhados.filter((e) => e.destinoPorta).map((e) => e.destinoPorta) + ).size; + const protocolosUnicos = new Set( + eventosDetalhados.filter((e) => e.protocolo).map((e) => e.protocolo) + ).size; + + // Status dos eventos + const porStatus = eventosDetalhados.reduce( + (acc, evento) => { + acc[evento.status] = (acc[evento.status] || 0) + 1; + return acc; + }, + {} as Record + ); + + doc.setFontSize(10); + doc.setFont('helvetica', 'normal'); + doc.text(`IPs únicos envolvidos: ${ipsUnicos}`, 20, yPosition); + yPosition += 7; + doc.text(`Portas únicas alvo: ${portasUnicas}`, 20, yPosition); + yPosition += 7; + doc.text(`Protocolos diferentes: ${protocolosUnicos}`, 20, yPosition); + yPosition += 10; + + // Tabela de status + if (Object.keys(porStatus).length > 0) { + doc.setFontSize(11); + doc.setFont('helvetica', 'bold'); + doc.text('Distribuição por Status', 20, yPosition); + yPosition += 8; + + const statusData = Object.entries(porStatus).map(([status, qtd]) => [ + status, + String(qtd) + ]); + + autoTable(doc, { + startY: yPosition, + head: [['Status', 'Quantidade']], + body: statusData, + theme: 'grid', + headStyles: { fillColor: [52, 73, 94], fontStyle: 'bold', textColor: [255, 255, 255] }, + styles: { fontSize: 9 }, + margin: { left: 20, right: 20 } + }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + yPosition = (doc as any).lastAutoTable.finalY + 10; + } + } + + // Rodapé em todas as páginas + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const pageCount = (doc as any).internal.getNumberOfPages(); + for (let i = 1; i <= pageCount; i++) { + doc.setPage(i); + doc.setFontSize(8); + doc.setFont('helvetica', 'normal'); + doc.setTextColor(128, 128, 128); + doc.text('SGSE - Sistema de Gerenciamento da Secretaria de Esportes', 105, 285, { + align: 'center' + }); + doc.text(`Gerado em: ${agoraStr}`, 105, 290, { align: 'center' }); + doc.text(`Página ${i} de ${pageCount}`, 195, 290, { align: 'right' }); + } + + // Salvar PDF + const nomeArquivo = `Relatorio_Seguranca_Detalhado_${new Date(r.criadoEm).toISOString().split('T')[0]}_${new Date().getTime()}.pdf`; + doc.save(nomeArquivo); + } catch (error) { + console.error('Erro ao gerar PDF:', error); + alert('Erro ao gerar PDF do relatório. Tente novamente.'); } - win.document.open(); - win.document.write(conteudo); - win.document.close(); } async function excluirRelatorio(relatorioId: Id<'reportRequests'>) { if (!confirm('Excluir este relatório? Esta ação não pode ser desfeita.')) return; @@ -1690,210 +1973,681 @@
-

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} -
- - -
- - +
+
+

Alertas e Notificações

+

+ Configure destinatários, níveis e tipos de alarme e reenvio para monitoramento de + segurança. +

-
-

Configurações salvas

+ + +
+ +
+
+
+ + + +

Notificações por Email

+
+ +
+ +
+ +
+
Emails de Destino
+
+
+ + +
+ +
+ +
+ +
+ {#if alertEmails.trim()} + {@const emailsAdicionados = alertEmails + .split('\n') + .map((s) => s.trim()) + .filter(Boolean)} + {#if emailsAdicionados.length > 0} +
+

+ Emails adicionados ({emailsAdicionados.length}): +

+
+ {#each emailsAdicionados as email (email)} + + {email} + + + {/each} +
+
+ {/if} + {/if} +
+
+
+ + +
+
Filtros de Alertas
+
+
+ + +
+ +
+ +
+ +
+ {#if alertTiposAtaque.length > 0} +
+ {#each alertTiposAtaque as tipo (tipo)} + + {attackLabels[tipo as AtaqueCiberneticoTipo]} + + + {/each} +
+ {/if} +
+
+
+ + +
+
Configurações de Envio
+
+
+ +
+ + minutos +
+
+ +
+ + +
+
+
+
+
+ + +
+
+
+ + + +

Notificações por Chat

+
+ +
+ +
+ +
+
Usuários do Chat
+
+
+ + +
+ +
+ +
+ +
+ {#if alertUsersChat.trim()} + {@const usuariosAdicionados = alertUsersChat + .split('\n') + .map((s) => s.trim()) + .filter(Boolean)} + {#if usuariosAdicionados.length > 0} +
+

+ Usuários adicionados ({usuariosAdicionados.length}): +

+
+ {#each usuariosAdicionados as usuario (usuario)} + + {usuario} + + + {/each} +
+
+ {/if} + {/if} +
+
+
+ + +
+
Filtros de Alertas
+
+
+ + +
+ +
+ +
+ +
+ {#if chatTiposAtaque.length > 0} +
+ {#each chatTiposAtaque as tipo (tipo)} + + {attackLabels[tipo as AtaqueCiberneticoTipo]} + + + {/each} +
+ {/if} +
+
+
+ + +
+
Configurações de Envio
+
+
+ +
+ + minutos +
+
+
+
+
+
+
+ + +
+ +
+ + +
+ 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} +
+
+
+
{cfg.nome}
+
+ {#if cfg.canais.email} + + + + + Email + + {/if} + {#if cfg.canais.chat} + + + + + Chat + + {/if} +
-
- - +
+
+ Severidade: + {severityLabels[cfg.severidadeMin]}
+
+ Reenvio: + {cfg.reenvioMin} minutos +
+ {#if cfg.emails.length} +
+ Emails ({cfg.emails.length}): +
+ {#each cfg.emails.slice(0, 3) as email (email)} + {email} + {/each} + {#if cfg.emails.length > 3} + +{cfg.emails.length - 3} + {/if} +
+
+ {/if} + {#if cfg.chatUsers.length} +
+ Usuários Chat ({cfg.chatUsers.length}): +
+ {#each cfg.chatUsers.slice(0, 2) as user (user)} + {user} + {/each} + {#if cfg.chatUsers.length > 2} + +{cfg.chatUsers.length - 2} + {/if} +
+
+ {/if} +
+
+ +
{/each}
{:else} -

Nenhuma configuração salva.

+
+ + + +

Nenhuma configuração salva

+

+ Configure e salve suas preferências de alertas acima +

+
{/if}