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.
This commit is contained in:
714
apps/web/src/lib/components/ti/CybersecurityWizcard.svelte
Normal file
714
apps/web/src/lib/components/ti/CybersecurityWizcard.svelte
Normal file
@@ -0,0 +1,714 @@
|
||||
<script lang="ts">
|
||||
import { browser } from '$app/environment';
|
||||
import { useQuery, useConvexClient } from 'convex-svelte';
|
||||
import type { FunctionReturnType } from 'convex/server';
|
||||
import type { Id } from '@sgse-app/backend/convex/_generated/dataModel';
|
||||
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';
|
||||
|
||||
type SerieCamada = FunctionReturnType<typeof api.security.obterVisaoCamadas>['series'][number];
|
||||
type VisaoCamadas = FunctionReturnType<typeof api.security.obterVisaoCamadas>;
|
||||
type EventosSeguranca = FunctionReturnType<typeof api.security.listarEventosSeguranca>;
|
||||
type ReputacoesData = FunctionReturnType<typeof api.security.listarReputacoes>;
|
||||
type RegrasPortaData = FunctionReturnType<typeof api.security.listarRegrasPorta>;
|
||||
|
||||
const client = useConvexClient();
|
||||
|
||||
const visaoCamadas = useQuery(api.security.obterVisaoCamadas, { periodoHoras: 6, buckets: 28 });
|
||||
const eventos = useQuery(api.security.listarEventosSeguranca, { limit: 120 });
|
||||
const reputacoes = useQuery(api.security.listarReputacoes, { limit: 60, lista: 'blacklist' });
|
||||
const regrasPorta = useQuery(api.security.listarRegrasPorta, {});
|
||||
|
||||
const severidadesDisponiveis: SeveridadeSeguranca[] = ['informativo', 'baixo', 'moderado', 'alto', 'critico'];
|
||||
|
||||
const severityLabels: Record<SeveridadeSeguranca, string> = {
|
||||
informativo: 'Informativo',
|
||||
baixo: 'Baixo',
|
||||
moderado: 'Moderado',
|
||||
alto: 'Alto',
|
||||
critico: 'Crítico'
|
||||
};
|
||||
|
||||
const severityStyles: Record<SeveridadeSeguranca, string> = {
|
||||
informativo: 'badge badge-ghost',
|
||||
baixo: 'badge badge-success',
|
||||
moderado: 'badge badge-info',
|
||||
alto: 'badge badge-warning',
|
||||
critico: 'badge badge-error'
|
||||
};
|
||||
|
||||
const attackLabels: Record<AtaqueCiberneticoTipo, string> = {
|
||||
phishing: 'Phishing',
|
||||
malware: 'Malware',
|
||||
ransomware: 'Ransomware',
|
||||
brute_force: 'Brute Force',
|
||||
credential_stuffing: 'Credential Stuffing',
|
||||
sql_injection: 'SQL Injection',
|
||||
xss: 'XSS',
|
||||
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 statusStyles: Record<string, string> = {
|
||||
detectado: 'badge badge-outline border-info/60 text-info',
|
||||
investigando: 'badge badge-secondary',
|
||||
contido: 'badge badge-success',
|
||||
'falso_positivo': 'badge badge-ghost',
|
||||
escalado: 'badge badge-error animate-pulse',
|
||||
resolvido: 'badge badge-neutral'
|
||||
};
|
||||
|
||||
let filtroSeveridades = $state<SeveridadeSeguranca[]>(['critico', 'alto']);
|
||||
let filtroTipos = $state<AtaqueCiberneticoTipo[]>([]);
|
||||
let alertaSonoroAtivo = $state(true);
|
||||
let alertaVisualAtivo = $state(true);
|
||||
let ipManual = $state('');
|
||||
let comentarioManual = $state('');
|
||||
let porta = $state(443);
|
||||
let protocolo = $state<'tcp' | 'udp' | 'icmp' | 'quic' | 'any'>('tcp');
|
||||
let acao = $state<'permitir' | 'bloquear' | 'monitorar' | 'rate_limit'>('monitorar');
|
||||
let temporario = $state(true);
|
||||
let duracao = $state(900);
|
||||
let severidadeMin = $state<SeveridadeSeguranca>('moderado');
|
||||
let relatorioInicio = $state(new Date(Date.now() - 60 * 60 * 1000).toISOString().slice(0, 16));
|
||||
let relatorioFim = $state(new Date().toISOString().slice(0, 16));
|
||||
let incluirMetricas = $state(true);
|
||||
let incluirAcoes = $state(true);
|
||||
let feedback = $state<{ tipo: 'success' | 'error'; mensagem: string } | null>(null);
|
||||
let ultimaReferenciaCritica: Id<'securityEvents'> | null = null;
|
||||
let audioCtx: AudioContext | null = null;
|
||||
|
||||
const series = $derived.by(() => visaoCamadas?.data?.series ?? []);
|
||||
const totais = $derived.by(() => visaoCamadas?.data?.totais ?? null);
|
||||
const eventosFiltrados = $derived.by(() => {
|
||||
const lista = eventos?.data ?? [];
|
||||
return lista
|
||||
.filter((evento) => {
|
||||
if (filtroSeveridades.length && !filtroSeveridades.includes(evento.severidade)) {
|
||||
return false;
|
||||
}
|
||||
if (filtroTipos.length && !filtroTipos.includes(evento.tipoAtaque)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.slice(0, 50);
|
||||
});
|
||||
const ipCriticos = $derived.by(() => (reputacoes?.data ?? []).slice(0, 10));
|
||||
const regras = $derived.by(() => regrasPorta?.data ?? []);
|
||||
|
||||
const eventoCriticoAtual = $derived.by(() => (eventos?.data ?? []).find((evento) => evento.severidade === 'critico') ?? null);
|
||||
|
||||
function maxSeriesValue(dataset: Array<Array<number>>): number {
|
||||
let max = 1;
|
||||
for (const serie of dataset) {
|
||||
for (const valor of serie) {
|
||||
if (valor > max) max = valor;
|
||||
}
|
||||
}
|
||||
return max;
|
||||
}
|
||||
|
||||
function construirLayer(valores: number[], largura: number, altura: number, maxValor: number): string {
|
||||
if (!valores.length) {
|
||||
return '';
|
||||
}
|
||||
const passo = valores.length > 1 ? largura / (valores.length - 1) : largura;
|
||||
const pontos = valores.map((valor, indice) => {
|
||||
const x = indice * passo;
|
||||
const proporcao = maxValor === 0 ? 0 : valor / maxValor;
|
||||
const y = altura - proporcao * altura;
|
||||
return `${x},${Number.isFinite(y) ? y : altura}`;
|
||||
});
|
||||
return `M0,${altura} ${pontos.map((ponto) => `L${ponto}`).join(' ')} L${largura},${altura} Z`;
|
||||
}
|
||||
|
||||
const chartPaths = $derived.by(() => {
|
||||
if (!series.length) {
|
||||
return { ddos: '', sql: '', avancados: '' };
|
||||
}
|
||||
const largura = 880;
|
||||
const altura = 220;
|
||||
const ddos = series.map((bucket) => bucket.ddos);
|
||||
const sql = series.map((bucket) => bucket.sqlInjection);
|
||||
const avancados = series.map((bucket) => bucket.avancados);
|
||||
const maxValor = maxSeriesValue([ddos, sql, avancados]);
|
||||
return {
|
||||
ddos: construirLayer(ddos, largura, altura, maxValor),
|
||||
sql: construirLayer(sql, largura, altura, maxValor),
|
||||
avancados: construirLayer(avancados, largura, altura, maxValor)
|
||||
};
|
||||
});
|
||||
|
||||
function toggleFiltroSeveridade(item: SeveridadeSeguranca) {
|
||||
if (filtroSeveridades.includes(item)) {
|
||||
filtroSeveridades = filtroSeveridades.filter((valor) => valor !== item);
|
||||
} else {
|
||||
filtroSeveridades = [...filtroSeveridades, item];
|
||||
}
|
||||
}
|
||||
|
||||
function toggleFiltroTipo(item: AtaqueCiberneticoTipo) {
|
||||
if (filtroTipos.includes(item)) {
|
||||
filtroTipos = filtroTipos.filter((valor) => valor !== item);
|
||||
} else {
|
||||
filtroTipos = [...filtroTipos, item];
|
||||
}
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (!eventoCriticoAtual || !alertaSonoroAtivo) {
|
||||
return;
|
||||
}
|
||||
if (ultimaReferenciaCritica === eventoCriticoAtual._id) {
|
||||
return;
|
||||
}
|
||||
ultimaReferenciaCritica = eventoCriticoAtual._id;
|
||||
if (!browser) {
|
||||
return;
|
||||
}
|
||||
if (!audioCtx) {
|
||||
audioCtx = new AudioContext();
|
||||
}
|
||||
const oscillator = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
oscillator.type = 'triangle';
|
||||
oscillator.frequency.value = 880;
|
||||
gain.gain.value = 0.08;
|
||||
oscillator.connect(gain).connect(audioCtx.destination);
|
||||
oscillator.start();
|
||||
setTimeout(() => {
|
||||
oscillator.stop();
|
||||
}, 300);
|
||||
});
|
||||
|
||||
function mensagemErro(erro: unknown): string {
|
||||
if (erro instanceof Error) return erro.message;
|
||||
return 'Operação não concluída.';
|
||||
}
|
||||
|
||||
function obterUsuarioId(): Id<'usuarios'> {
|
||||
if (!authStore.usuario?._id) {
|
||||
throw new Error('Usuário não autenticado.');
|
||||
}
|
||||
return authStore.usuario._id as Id<'usuarios'>;
|
||||
}
|
||||
|
||||
async function aplicarMedidaIp(acaoSelecionada: 'forcar_blacklist' | 'remover_blacklist', ipAlvo: string, eventoId?: Id<'securityEvents'>) {
|
||||
if (!ipAlvo) {
|
||||
feedback = { tipo: 'error', mensagem: 'Informe o IP alvo.' };
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await client.mutation(api.security.atualizarReputacaoIndicador, {
|
||||
usuarioId: obterUsuarioId(),
|
||||
indicador: ipAlvo,
|
||||
categoria: 'ip',
|
||||
acao: acaoSelecionada,
|
||||
delta: acaoSelecionada === 'forcar_blacklist' ? -30 : 10,
|
||||
comentario: comentarioManual || `Ação manual em ${new Date().toLocaleString()}`,
|
||||
duracaoSegundos: acaoSelecionada === 'forcar_blacklist' ? 3600 : undefined
|
||||
});
|
||||
|
||||
if (eventoId) {
|
||||
await client.mutation(api.security.registrarAcaoIncidente, {
|
||||
eventoId,
|
||||
tipo: acaoSelecionada === 'forcar_blacklist' ? 'block_ip' : 'unblock_ip',
|
||||
origem: 'manual',
|
||||
executadoPor: obterUsuarioId(),
|
||||
detalhes: comentarioManual || 'Ação executada via Wizcard',
|
||||
resultado: 'registrado',
|
||||
relacionadoA: undefined
|
||||
});
|
||||
}
|
||||
|
||||
feedback = {
|
||||
tipo: 'success',
|
||||
mensagem:
|
||||
acaoSelecionada === 'forcar_blacklist'
|
||||
? `IP ${ipAlvo} bloqueado e registrado na blacklist.`
|
||||
: `IP ${ipAlvo} liberado e removido da blacklist.`
|
||||
};
|
||||
ipManual = '';
|
||||
comentarioManual = '';
|
||||
} catch (erro: unknown) {
|
||||
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
|
||||
}
|
||||
}
|
||||
|
||||
async function salvarRegraPorta(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await client.mutation(api.security.configurarRegraPorta, {
|
||||
usuarioId: obterUsuarioId(),
|
||||
porta,
|
||||
protocolo,
|
||||
acao,
|
||||
temporario,
|
||||
duracaoSegundos: temporario ? duracao : undefined,
|
||||
severidadeMin,
|
||||
notas: `Regra criada via Wizcard em ${new Date().toLocaleString()}`,
|
||||
tags: ['cibersecurity', acao],
|
||||
listaReferencia: undefined
|
||||
});
|
||||
feedback = { tipo: 'success', mensagem: `Regra para porta ${porta}/${protocolo.toUpperCase()} registrada.` };
|
||||
} catch (erro: unknown) {
|
||||
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
|
||||
}
|
||||
}
|
||||
|
||||
function parseDatetime(valor: string): number {
|
||||
if (!valor) return Date.now();
|
||||
const data = new Date(valor);
|
||||
return Number.isNaN(data.getTime()) ? Date.now() : data.getTime();
|
||||
}
|
||||
|
||||
async function gerarRelatorioAvancado(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
try {
|
||||
await client.mutation(api.security.solicitarRelatorioSeguranca, {
|
||||
solicitanteId: obterUsuarioId(),
|
||||
filtros: {
|
||||
dataInicio: parseDatetime(relatorioInicio),
|
||||
dataFim: parseDatetime(relatorioFim),
|
||||
severidades: filtroSeveridades.length ? filtroSeveridades : undefined,
|
||||
tiposAtaque: filtroTipos.length ? filtroTipos : undefined,
|
||||
incluirIndicadores: true,
|
||||
incluirMetricas,
|
||||
incluirAcoes
|
||||
}
|
||||
});
|
||||
feedback = {
|
||||
tipo: 'success',
|
||||
mensagem: 'Relatório refinado em processamento. Você será notificado em instantes.'
|
||||
};
|
||||
} catch (erro: unknown) {
|
||||
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
|
||||
}
|
||||
}
|
||||
|
||||
function classeEventoCritico(severidade: SeveridadeSeguranca): string {
|
||||
if (!alertaVisualAtivo) return '';
|
||||
return severidade === 'critico' ? 'ring-2 ring-error animate-pulse' : '';
|
||||
}
|
||||
|
||||
function formatarData(timestamp: number): string {
|
||||
const data = new Date(timestamp);
|
||||
return data.toLocaleString('pt-BR', {
|
||||
hour12: false
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
{#if feedback}
|
||||
<div class="alert shadow-lg {feedback.tipo === 'success' ? 'alert-success' : 'alert-error'}">
|
||||
<div>
|
||||
<span>{feedback.mensagem}</span>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-ghost" onclick={() => (feedback = null)}>Fechar</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<section class="grid gap-4 lg:grid-cols-4 md:grid-cols-2">
|
||||
<div class="stat bg-base-100 rounded-2xl border border-primary/20 shadow-xl">
|
||||
<div class="stat-title text-primary font-semibold">Eventos monitorados</div>
|
||||
<div class="stat-value text-primary text-4xl">{totais?.eventos ?? 0}</div>
|
||||
<div class="stat-desc">Últimas 6h</div>
|
||||
</div>
|
||||
<div class="stat bg-base-100 rounded-2xl border border-error/20 shadow-xl">
|
||||
<div class="stat-title text-error font-semibold">Críticos</div>
|
||||
<div class="stat-value text-error text-4xl">{totais?.criticos ?? 0}</div>
|
||||
<div class="stat-desc">Escalonados imediatamente</div>
|
||||
</div>
|
||||
<div class="stat bg-base-100 rounded-2xl border border-warning/20 shadow-xl">
|
||||
<div class="stat-title text-warning font-semibold">Bloqueios ativos</div>
|
||||
<div class="stat-value text-warning text-4xl">{totais?.bloqueiosAtivos ?? 0}</div>
|
||||
<div class="stat-desc">IPs e domínios isolados</div>
|
||||
</div>
|
||||
<div class="stat bg-base-100 rounded-2xl border border-success/20 shadow-xl">
|
||||
<div class="stat-title text-success font-semibold">Sensores ativos</div>
|
||||
<div class="stat-value text-success text-4xl">{totais?.sensoresAtivos ?? 0}</div>
|
||||
<div class="stat-desc">Edge, OT e honeypots</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="rounded-3xl border border-primary/20 bg-base-100/80 p-6 shadow-2xl lg:col-span-2">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-2xl font-bold text-primary">Layerchart Threat Matrix</h2>
|
||||
<p class="text-base-content/70 text-sm">
|
||||
Correlação temporal entre DDoS, SQLi, ataques avançados e bloqueios automáticos.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<span class="label-text">Alerta Sonoro</span>
|
||||
<input type="checkbox" class="toggle toggle-primary" bind:checked={alertaSonoroAtivo} />
|
||||
</label>
|
||||
<label class="label cursor-pointer gap-2">
|
||||
<span class="label-text">Flash Visual</span>
|
||||
<input type="checkbox" class="toggle toggle-secondary" bind:checked={alertaVisualAtivo} />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 overflow-x-auto rounded-2xl bg-base-200/40 p-4">
|
||||
<svg viewBox="0 0 880 220" class="w-full">
|
||||
<defs>
|
||||
<linearGradient id="grad-ddos" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="rgba(255, 76, 76, 0.8)" />
|
||||
<stop offset="100%" stop-color="rgba(255, 76, 76, 0.1)" />
|
||||
</linearGradient>
|
||||
<linearGradient id="grad-sql" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="rgba(59, 130, 246, 0.8)" />
|
||||
<stop offset="100%" stop-color="rgba(59, 130, 246, 0.1)" />
|
||||
</linearGradient>
|
||||
<linearGradient id="grad-adv" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||
<stop offset="0%" stop-color="rgba(236, 72, 153, 0.8)" />
|
||||
<stop offset="100%" stop-color="rgba(236, 72, 153, 0.1)" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d={chartPaths.avancados} fill="url(#grad-adv)" stroke="rgba(236, 72, 153, 0.6)" stroke-width="2" />
|
||||
<path d={chartPaths.sql} fill="url(#grad-sql)" stroke="rgba(59, 130, 246, 0.6)" stroke-width="2" />
|
||||
<path d={chartPaths.ddos} fill="url(#grad-ddos)" stroke="rgba(248, 113, 113, 0.8)" stroke-width="2" />
|
||||
</svg>
|
||||
<div class="mt-4 flex flex-wrap gap-3 text-sm font-semibold">
|
||||
<span class="badge badge-error gap-2">
|
||||
<span class="h-2 w-2 rounded-full bg-error"></span>DDoS
|
||||
</span>
|
||||
<span class="badge badge-info gap-2">
|
||||
<span class="h-2 w-2 rounded-full bg-info"></span>SQL Injection
|
||||
</span>
|
||||
<span class="badge badge-secondary gap-2">
|
||||
<span class="h-2 w-2 rounded-full bg-secondary"></span>ATA Advanced
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex flex-wrap gap-2">
|
||||
{#each severidadesDisponiveis as severidade}
|
||||
<button
|
||||
type="button"
|
||||
class={`btn btn-xs ${filtroSeveridades.includes(severidade) ? 'btn-primary' : 'btn-outline btn-primary'}`}
|
||||
onclick={() => toggleFiltroSeveridade(severidade)}
|
||||
>
|
||||
{severityLabels[severidade]}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-3xl border border-secondary/20 bg-base-100/80 p-6 shadow-2xl">
|
||||
<h3 class="text-secondary text-xl font-bold">Ações rápidas</h3>
|
||||
<form
|
||||
class="mt-4 space-y-3"
|
||||
onsubmit={(event) => {
|
||||
event.preventDefault();
|
||||
aplicarMedidaIp('forcar_blacklist', ipManual);
|
||||
}}
|
||||
>
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">IP / Host</span>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Ex: 177.24.10.90"
|
||||
bind:value={ipManual}
|
||||
/>
|
||||
</label>
|
||||
<label class="form-control w-full">
|
||||
<div class="label">
|
||||
<span class="label-text">Comentário técnico</span>
|
||||
</div>
|
||||
<textarea
|
||||
class="textarea textarea-bordered w-full"
|
||||
rows="2"
|
||||
placeholder="Ex: Bloqueio após heurística MITM no JA3."
|
||||
bind:value={comentarioManual}
|
||||
></textarea>
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="btn btn-error flex-1">Bloquear IP</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success flex-1"
|
||||
onclick={() => aplicarMedidaIp('remover_blacklist', ipManual)}
|
||||
>
|
||||
Liberar
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="divider my-6">Regras de Porta</div>
|
||||
<form class="space-y-3" onsubmit={salvarRegraPorta}>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<label class="form-control">
|
||||
<div class="label">
|
||||
<span class="label-text">Porta</span>
|
||||
</div>
|
||||
<input type="number" class="input input-bordered" min="1" max="65535" bind:value={porta} />
|
||||
</label>
|
||||
<label class="form-control">
|
||||
<div class="label">
|
||||
<span class="label-text">Protocolo</span>
|
||||
</div>
|
||||
<select class="select select-bordered" bind:value={protocolo}>
|
||||
<option value="tcp">TCP</option>
|
||||
<option value="udp">UDP</option>
|
||||
<option value="icmp">ICMP</option>
|
||||
<option value="quic">QUIC</option>
|
||||
<option value="any">ANY</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<label class="form-control">
|
||||
<div class="label">
|
||||
<span class="label-text">Ação</span>
|
||||
</div>
|
||||
<select class="select select-bordered" bind:value={acao}>
|
||||
<option value="permitir">Permitir</option>
|
||||
<option value="bloquear">Bloquear</option>
|
||||
<option value="monitorar">Monitorar</option>
|
||||
<option value="rate_limit">Rate Limit</option>
|
||||
</select>
|
||||
</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="checkbox" class="toggle toggle-info" bind:checked={temporario} />
|
||||
<span class="label-text">Bloqueio temporário?</span>
|
||||
{#if temporario}
|
||||
<input
|
||||
type="number"
|
||||
class="input input-bordered ml-auto w-32"
|
||||
min="60"
|
||||
step="60"
|
||||
placeholder="Segundos"
|
||||
bind:value={duracao}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<label class="form-control">
|
||||
<div class="label">
|
||||
<span class="label-text">Severidade mínima</span>
|
||||
</div>
|
||||
<select class="select select-bordered" bind:value={severidadeMin}>
|
||||
{#each severidadesDisponiveis as severidade}
|
||||
<option value={severidade}>{severityLabels[severidade]}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
<button type="submit" class="btn btn-secondary w-full">Salvar Regra</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-6 lg:grid-cols-3">
|
||||
<div class="rounded-3xl border border-base-300 bg-base-100 p-6 shadow-2xl lg:col-span-2">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<h3 class="text-2xl font-semibold text-base-content">Feed de eventos e ameaças</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each Object.entries(attackLabels).slice(0, 8) as [tipo, label]}
|
||||
<button
|
||||
type="button"
|
||||
class={`btn btn-xs ${filtroTipos.includes(tipo as AtaqueCiberneticoTipo) ? 'btn-accent' : 'btn-outline btn-accent'}`}
|
||||
onclick={() => toggleFiltroTipo(tipo as AtaqueCiberneticoTipo)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 space-y-4">
|
||||
{#if eventosFiltrados.length === 0}
|
||||
<p class="text-base-content/60 text-sm">Nenhum evento correspondente aos filtros.</p>
|
||||
{:else}
|
||||
{#each eventosFiltrados as evento}
|
||||
<div class={`card border border-base-200 bg-base-100 shadow-lg ${classeEventoCritico(evento.severidade)}`}>
|
||||
<div class="card-body gap-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class={severityStyles[evento.severidade]}>{severityLabels[evento.severidade]}</span>
|
||||
<span class="badge badge-outline">{attackLabels[evento.tipoAtaque]}</span>
|
||||
<span class={statusStyles[evento.status] ?? 'badge badge-ghost'}>{evento.status}</span>
|
||||
</div>
|
||||
<span class="text-xs text-base-content/60">{formatarData(evento.timestamp)}</span>
|
||||
</div>
|
||||
<p class="text-sm text-base-content/80">{evento.descricao}</p>
|
||||
<div class="grid gap-2 text-xs md:grid-cols-2">
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold">Origem:</span>
|
||||
<span>{evento.origemIp ?? 'n/d'}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold">Destino:</span>
|
||||
<span>{evento.destinoIp ?? 'n/d'}:{evento.destinoPorta ?? '--'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold">Protocolo:</span>
|
||||
<span>{evento.protocolo ?? 'n/d'}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold">Tags:</span>
|
||||
<span>{evento.tags?.join(', ') ?? '—'}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-error btn-sm"
|
||||
onclick={() => aplicarMedidaIp('forcar_blacklist', evento.origemIp ?? '', evento._id)}
|
||||
>
|
||||
Bloquear origem
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline btn-sm"
|
||||
onclick={async () => {
|
||||
try {
|
||||
await client.mutation(api.security.solicitarRelatorioSeguranca, {
|
||||
solicitanteId: obterUsuarioId(),
|
||||
filtros: {
|
||||
dataInicio: evento.timestamp - 15 * 60 * 1000,
|
||||
dataFim: evento.timestamp + 15 * 60 * 1000,
|
||||
severidades: [evento.severidade],
|
||||
tiposAtaque: [evento.tipoAtaque],
|
||||
incluirIndicadores: true,
|
||||
incluirMetricas: true,
|
||||
incluirAcoes: true
|
||||
}
|
||||
});
|
||||
feedback = {
|
||||
tipo: 'success',
|
||||
mensagem: 'Mini relatório solicitado para o evento selecionado.'
|
||||
};
|
||||
} catch (erro: unknown) {
|
||||
feedback = { tipo: 'error', mensagem: mensagemErro(erro) };
|
||||
}
|
||||
}}
|
||||
>
|
||||
Relatório 30min
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-3xl border border-accent/20 bg-base-100 p-6 shadow-2xl space-y-6">
|
||||
<div>
|
||||
<h4 class="text-accent text-lg font-bold">Lista Negra Inteligente</h4>
|
||||
<ul class="mt-4 space-y-3 text-sm">
|
||||
{#if ipCriticos.length === 0}
|
||||
<li class="text-base-content/60">Nenhum IP crítico listado.</li>
|
||||
{:else}
|
||||
{#each ipCriticos as registro}
|
||||
<li class="flex items-center justify-between gap-2 rounded-xl bg-base-200/40 p-3">
|
||||
<div>
|
||||
<p class="font-semibold">{registro.indicador}</p>
|
||||
<p class="text-xs text-base-content/60">
|
||||
Score: {registro.reputacao} • Ocorrências: {registro.ocorrencias}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-xs btn-outline"
|
||||
onclick={() => aplicarMedidaIp('remover_blacklist', registro.indicador)}
|
||||
>
|
||||
Liberar
|
||||
</button>
|
||||
</li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-info text-lg font-bold">Regras de Portas Monitoradas</h4>
|
||||
<div class="mt-4 space-y-2 text-xs">
|
||||
{#if regras.length === 0}
|
||||
<p class="text-base-content/60">Nenhuma regra cadastrada.</p>
|
||||
{:else}
|
||||
{#each regras as regra}
|
||||
<div class="rounded-xl border border-base-200 p-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold">
|
||||
{regra.porta}/{regra.protocolo.toUpperCase()}
|
||||
</span>
|
||||
<span class="badge badge-outline">{regra.acao}</span>
|
||||
</div>
|
||||
<p class="text-base-content/70 mt-1">
|
||||
Severidade mínima: {severityLabels[regra.severidadeMin]}
|
||||
</p>
|
||||
{#if regra.expiraEm}
|
||||
<p class="text-base-content/50">
|
||||
Expira em: {new Date(regra.expiraEm).toLocaleString('pt-BR', { hour12: false })}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 class="text-primary text-lg font-bold">Relatórios refinados</h4>
|
||||
<form class="mt-3 space-y-2" onsubmit={gerarRelatorioAvancado}>
|
||||
<label class="form-control">
|
||||
<div class="label">
|
||||
<span class="label-text text-xs">Início</span>
|
||||
</div>
|
||||
<input type="datetime-local" class="input input-bordered input-sm" bind:value={relatorioInicio} />
|
||||
</label>
|
||||
<label class="form-control">
|
||||
<div class="label">
|
||||
<span class="label-text text-xs">Fim</span>
|
||||
</div>
|
||||
<input type="datetime-local" class="input input-bordered input-sm" bind:value={relatorioFim} />
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-xs">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" bind:checked={incluirMetricas} />
|
||||
<span>Incluir métricas e payload hash</span>
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-xs">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" bind:checked={incluirAcoes} />
|
||||
<span>Incluir histórico de ações</span>
|
||||
</label>
|
||||
<button type="submit" class="btn btn-primary btn-sm w-full">Gerar Relatório</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.card::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user