Feat cibersecurity #27
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>
|
||||
|
||||
@@ -1,485 +1,487 @@
|
||||
<script lang="ts">
|
||||
type HighlightVariant = "solid" | "outline";
|
||||
type FeatureIcon =
|
||||
| "control"
|
||||
| "support"
|
||||
| "shieldCheck"
|
||||
| "envelope"
|
||||
| "users"
|
||||
| "bell"
|
||||
| "monitor"
|
||||
| "document"
|
||||
| "teams"
|
||||
| "userPlus";
|
||||
type PaletteKey =
|
||||
| "primary"
|
||||
| "success"
|
||||
| "secondary"
|
||||
| "accent"
|
||||
| "info"
|
||||
| "error"
|
||||
| "warning";
|
||||
import { resolve } from '$app/paths';
|
||||
type HighlightVariant = 'solid' | 'outline';
|
||||
type FeatureIcon =
|
||||
| 'control'
|
||||
| 'support'
|
||||
| 'shieldCheck'
|
||||
| 'envelope'
|
||||
| 'users'
|
||||
| 'bell'
|
||||
| 'monitor'
|
||||
| 'document'
|
||||
| 'teams'
|
||||
| 'userPlus';
|
||||
type PaletteKey = 'primary' | 'success' | 'secondary' | 'accent' | 'info' | 'error' | 'warning';
|
||||
|
||||
type FeatureCard = {
|
||||
title: string;
|
||||
description: string;
|
||||
ctaLabel: string;
|
||||
href?: string;
|
||||
disabled?: boolean;
|
||||
palette: PaletteKey;
|
||||
icon: FeatureIcon;
|
||||
highlightBadges?: Array<{ label: string; variant: HighlightVariant }>;
|
||||
};
|
||||
type TiRouteId =
|
||||
| '/(dashboard)/ti/painel-administrativo'
|
||||
| '/(dashboard)/ti/cibersecurity'
|
||||
| '/(dashboard)/ti/central-chamados'
|
||||
| '/(dashboard)/ti/painel-permissoes'
|
||||
| '/(dashboard)/ti/configuracoes-email'
|
||||
| '/(dashboard)/ti/monitoramento-emails'
|
||||
| '/(dashboard)/ti/usuarios'
|
||||
| '/(dashboard)/ti/solicitacoes-acesso'
|
||||
| '/(dashboard)/ti/times'
|
||||
| '/(dashboard)/ti/notificacoes'
|
||||
| '/(dashboard)/ti/monitoramento';
|
||||
|
||||
type IconPath = {
|
||||
d: string;
|
||||
strokeLinecap?: "butt" | "round" | "square";
|
||||
strokeLinejoin?: "miter" | "round" | "bevel";
|
||||
strokeWidth?: number;
|
||||
};
|
||||
type FeatureCard = {
|
||||
title: string;
|
||||
description: string;
|
||||
ctaLabel: string;
|
||||
href?: TiRouteId;
|
||||
disabled?: boolean;
|
||||
palette: PaletteKey;
|
||||
icon: FeatureIcon;
|
||||
highlightBadges?: Array<{ label: string; variant: HighlightVariant }>;
|
||||
};
|
||||
|
||||
const paletteStyles: Record<
|
||||
PaletteKey,
|
||||
{
|
||||
cardBorder: string;
|
||||
iconBg: string;
|
||||
iconRing: string;
|
||||
iconColor: string;
|
||||
button: string;
|
||||
badgeSolid: string;
|
||||
badgeOutline: string;
|
||||
}
|
||||
> = {
|
||||
primary: {
|
||||
cardBorder: "border-primary/25",
|
||||
iconBg: "bg-primary/15",
|
||||
iconRing: "ring-1 ring-primary/30",
|
||||
iconColor: "text-primary",
|
||||
button: "btn-primary",
|
||||
badgeSolid: "badge-primary text-primary-content",
|
||||
badgeOutline: "badge-outline border-primary/30",
|
||||
},
|
||||
success: {
|
||||
cardBorder: "border-success/25",
|
||||
iconBg: "bg-success/15",
|
||||
iconRing: "ring-1 ring-success/25",
|
||||
iconColor: "text-success",
|
||||
button: "btn-success",
|
||||
badgeSolid: "badge-success text-success-content",
|
||||
badgeOutline: "badge-outline border-success/30",
|
||||
},
|
||||
secondary: {
|
||||
cardBorder: "border-secondary/25",
|
||||
iconBg: "bg-secondary/15",
|
||||
iconRing: "ring-1 ring-secondary/25",
|
||||
iconColor: "text-secondary",
|
||||
button: "btn-secondary",
|
||||
badgeSolid: "badge-secondary text-secondary-content",
|
||||
badgeOutline: "badge-outline border-secondary/30",
|
||||
},
|
||||
accent: {
|
||||
cardBorder: "border-accent/25",
|
||||
iconBg: "bg-accent/15",
|
||||
iconRing: "ring-1 ring-accent/20",
|
||||
iconColor: "text-accent",
|
||||
button: "btn-accent",
|
||||
badgeSolid: "badge-accent text-accent-content",
|
||||
badgeOutline: "badge-outline border-accent/30",
|
||||
},
|
||||
info: {
|
||||
cardBorder: "border-info/25",
|
||||
iconBg: "bg-info/15",
|
||||
iconRing: "ring-1 ring-info/25",
|
||||
iconColor: "text-info",
|
||||
button: "btn-info",
|
||||
badgeSolid: "badge-info text-info-content",
|
||||
badgeOutline: "badge-outline border-info/30",
|
||||
},
|
||||
error: {
|
||||
cardBorder: "border-error/20",
|
||||
iconBg: "bg-error/15",
|
||||
iconRing: "ring-1 ring-error/25",
|
||||
iconColor: "text-error",
|
||||
button: "btn-error",
|
||||
badgeSolid: "badge-error text-error-content",
|
||||
badgeOutline: "badge-outline border-error/30",
|
||||
},
|
||||
warning: {
|
||||
cardBorder: "border-warning/25",
|
||||
iconBg: "bg-warning/15",
|
||||
iconRing: "ring-1 ring-warning/25",
|
||||
iconColor: "text-warning",
|
||||
button: "btn-warning",
|
||||
badgeSolid: "badge-warning text-warning-content",
|
||||
badgeOutline: "badge-outline border-warning/30",
|
||||
},
|
||||
};
|
||||
type IconPath = {
|
||||
d: string;
|
||||
strokeLinecap?: 'butt' | 'round' | 'square';
|
||||
strokeLinejoin?: 'miter' | 'round' | 'bevel';
|
||||
strokeWidth?: number;
|
||||
};
|
||||
|
||||
const iconPaths = {
|
||||
control: [
|
||||
{
|
||||
d: "M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
},
|
||||
],
|
||||
support: [
|
||||
{
|
||||
d: "M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
},
|
||||
],
|
||||
shieldCheck: [
|
||||
{
|
||||
d: "M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
},
|
||||
],
|
||||
envelope: [
|
||||
{
|
||||
d: "M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
},
|
||||
],
|
||||
users: [
|
||||
{
|
||||
d: "M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
},
|
||||
],
|
||||
bell: [
|
||||
{
|
||||
d: "M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
},
|
||||
],
|
||||
monitor: [
|
||||
{
|
||||
d: "M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
},
|
||||
],
|
||||
document: [
|
||||
{
|
||||
d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
},
|
||||
],
|
||||
teams: [
|
||||
{
|
||||
d: "M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
},
|
||||
],
|
||||
userPlus: [
|
||||
{
|
||||
d: "M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z",
|
||||
strokeLinecap: "round",
|
||||
strokeLinejoin: "round",
|
||||
},
|
||||
],
|
||||
} satisfies Record<FeatureIcon, IconPath[]>;
|
||||
const paletteStyles: Record<
|
||||
PaletteKey,
|
||||
{
|
||||
cardBorder: string;
|
||||
iconBg: string;
|
||||
iconRing: string;
|
||||
iconColor: string;
|
||||
button: string;
|
||||
badgeSolid: string;
|
||||
badgeOutline: string;
|
||||
}
|
||||
> = {
|
||||
primary: {
|
||||
cardBorder: 'border-primary/25',
|
||||
iconBg: 'bg-primary/15',
|
||||
iconRing: 'ring-1 ring-primary/30',
|
||||
iconColor: 'text-primary',
|
||||
button: 'btn-primary',
|
||||
badgeSolid: 'badge-primary text-primary-content',
|
||||
badgeOutline: 'badge-outline border-primary/30'
|
||||
},
|
||||
success: {
|
||||
cardBorder: 'border-success/25',
|
||||
iconBg: 'bg-success/15',
|
||||
iconRing: 'ring-1 ring-success/25',
|
||||
iconColor: 'text-success',
|
||||
button: 'btn-success',
|
||||
badgeSolid: 'badge-success text-success-content',
|
||||
badgeOutline: 'badge-outline border-success/30'
|
||||
},
|
||||
secondary: {
|
||||
cardBorder: 'border-secondary/25',
|
||||
iconBg: 'bg-secondary/15',
|
||||
iconRing: 'ring-1 ring-secondary/25',
|
||||
iconColor: 'text-secondary',
|
||||
button: 'btn-secondary',
|
||||
badgeSolid: 'badge-secondary text-secondary-content',
|
||||
badgeOutline: 'badge-outline border-secondary/30'
|
||||
},
|
||||
accent: {
|
||||
cardBorder: 'border-accent/25',
|
||||
iconBg: 'bg-accent/15',
|
||||
iconRing: 'ring-1 ring-accent/20',
|
||||
iconColor: 'text-accent',
|
||||
button: 'btn-accent',
|
||||
badgeSolid: 'badge-accent text-accent-content',
|
||||
badgeOutline: 'badge-outline border-accent/30'
|
||||
},
|
||||
info: {
|
||||
cardBorder: 'border-info/25',
|
||||
iconBg: 'bg-info/15',
|
||||
iconRing: 'ring-1 ring-info/25',
|
||||
iconColor: 'text-info',
|
||||
button: 'btn-info',
|
||||
badgeSolid: 'badge-info text-info-content',
|
||||
badgeOutline: 'badge-outline border-info/30'
|
||||
},
|
||||
error: {
|
||||
cardBorder: 'border-error/20',
|
||||
iconBg: 'bg-error/15',
|
||||
iconRing: 'ring-1 ring-error/25',
|
||||
iconColor: 'text-error',
|
||||
button: 'btn-error',
|
||||
badgeSolid: 'badge-error text-error-content',
|
||||
badgeOutline: 'badge-outline border-error/30'
|
||||
},
|
||||
warning: {
|
||||
cardBorder: 'border-warning/25',
|
||||
iconBg: 'bg-warning/15',
|
||||
iconRing: 'ring-1 ring-warning/25',
|
||||
iconColor: 'text-warning',
|
||||
button: 'btn-warning',
|
||||
badgeSolid: 'badge-warning text-warning-content',
|
||||
badgeOutline: 'badge-outline border-warning/30'
|
||||
}
|
||||
};
|
||||
|
||||
const featureCards: Array<FeatureCard> = [
|
||||
{
|
||||
title: "Painel Administrativo",
|
||||
description:
|
||||
"Acesso restrito para gerenciamento de solicitações de acesso ao sistema e outras configurações administrativas.",
|
||||
ctaLabel: "Acessar Painel",
|
||||
href: "/ti/painel-administrativo",
|
||||
palette: "primary",
|
||||
icon: "control",
|
||||
},
|
||||
{
|
||||
title: "Central de Chamados",
|
||||
description:
|
||||
"Monitore tickets, configure SLA, atribua responsáveis e acompanhe alertas de prazos.",
|
||||
ctaLabel: "Abrir Central",
|
||||
href: "/ti/central-chamados",
|
||||
palette: "info",
|
||||
icon: "support",
|
||||
highlightBadges: [
|
||||
{ label: "SLA", variant: "solid" },
|
||||
{ label: "Alertas", variant: "outline" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Suporte Técnico",
|
||||
description:
|
||||
"Central de atendimento para resolução de problemas técnicos e dúvidas sobre o sistema.",
|
||||
ctaLabel: "Em breve",
|
||||
palette: "info",
|
||||
icon: "support",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
title: "Gerenciar Permissões",
|
||||
description:
|
||||
"Configure as permissões de acesso aos menus do sistema por função. Controle quem pode acessar, consultar e gravar dados.",
|
||||
ctaLabel: "Configurar Permissões",
|
||||
href: "/ti/painel-permissoes",
|
||||
palette: "success",
|
||||
icon: "shieldCheck",
|
||||
},
|
||||
{
|
||||
title: "Configuração de Email",
|
||||
description:
|
||||
"Configure o servidor SMTP para envio automático de notificações e emails do sistema.",
|
||||
ctaLabel: "Configurar SMTP",
|
||||
href: "/ti/configuracoes-email",
|
||||
palette: "secondary",
|
||||
icon: "envelope",
|
||||
},
|
||||
{
|
||||
title: "Monitoramento de Emails",
|
||||
description:
|
||||
"Acompanhe o status da fila de emails, identifique problemas de envio e processe manualmente quando necessário.",
|
||||
ctaLabel: "Monitorar Emails",
|
||||
href: "/ti/monitoramento-emails",
|
||||
palette: "info",
|
||||
icon: "envelope",
|
||||
highlightBadges: [
|
||||
{ label: "Tempo Real", variant: "solid" },
|
||||
{ label: "Debug", variant: "outline" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Gerenciar Usuários",
|
||||
description:
|
||||
"Criar, editar, bloquear e gerenciar usuários do sistema. Controle total sobre contas de acesso.",
|
||||
ctaLabel: "Gerenciar Usuários",
|
||||
href: "/ti/usuarios",
|
||||
palette: "accent",
|
||||
icon: "users",
|
||||
},
|
||||
{
|
||||
title: "Solicitações de Acesso",
|
||||
description:
|
||||
"Gerencie e analise solicitações de acesso ao sistema. Aprove ou rejeite novas solicitações de forma eficiente.",
|
||||
ctaLabel: "Gerenciar Solicitações",
|
||||
href: "/ti/solicitacoes-acesso",
|
||||
palette: "warning",
|
||||
icon: "userPlus",
|
||||
},
|
||||
{
|
||||
title: "Gestão de Times",
|
||||
description:
|
||||
"Organize funcionários em equipes e defina gestores. Gerencie membros e estrutura organizacional do sistema.",
|
||||
ctaLabel: "Gerenciar Times",
|
||||
href: "/ti/times",
|
||||
palette: "success",
|
||||
icon: "teams",
|
||||
},
|
||||
{
|
||||
title: "Notificações e Mensagens",
|
||||
description:
|
||||
"Envie notificações para usuários do sistema via chat ou email. Configure templates de mensagens reutilizáveis.",
|
||||
ctaLabel: "Acessar Painel",
|
||||
href: "/ti/notificacoes",
|
||||
palette: "info",
|
||||
icon: "bell",
|
||||
},
|
||||
{
|
||||
title: "Monitorar SGSE",
|
||||
description:
|
||||
"Monitore em tempo real as métricas técnicas do sistema e configure alertas inteligentes para a equipe de TI.",
|
||||
ctaLabel: "Monitorar Sistema",
|
||||
href: "/ti/monitoramento",
|
||||
palette: "error",
|
||||
icon: "monitor",
|
||||
highlightBadges: [
|
||||
{ label: "Tempo Real", variant: "solid" },
|
||||
{ label: "Alertas", variant: "outline" },
|
||||
{ label: "Relatórios", variant: "outline" },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Documentação",
|
||||
description:
|
||||
"Manuais, guias e documentação técnica do sistema para usuários e administradores.",
|
||||
ctaLabel: "Em breve",
|
||||
palette: "primary",
|
||||
icon: "document",
|
||||
disabled: true,
|
||||
},
|
||||
];
|
||||
const iconPaths: Record<FeatureIcon, IconPath[]> = {
|
||||
control: [
|
||||
{
|
||||
d: 'M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round'
|
||||
}
|
||||
],
|
||||
support: [
|
||||
{
|
||||
d: 'M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round'
|
||||
}
|
||||
],
|
||||
shieldCheck: [
|
||||
{
|
||||
d: 'M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round'
|
||||
}
|
||||
],
|
||||
envelope: [
|
||||
{
|
||||
d: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round'
|
||||
}
|
||||
],
|
||||
users: [
|
||||
{
|
||||
d: 'M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round'
|
||||
}
|
||||
],
|
||||
bell: [
|
||||
{
|
||||
d: 'M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round'
|
||||
}
|
||||
],
|
||||
monitor: [
|
||||
{
|
||||
d: 'M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round'
|
||||
}
|
||||
],
|
||||
document: [
|
||||
{
|
||||
d: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round'
|
||||
}
|
||||
],
|
||||
teams: [
|
||||
{
|
||||
d: 'M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round'
|
||||
}
|
||||
],
|
||||
userPlus: [
|
||||
{
|
||||
d: 'M18 9v3m0 0v3m0-3h3m-3 0h-3m-2-5a4 4 0 11-8 0 4 4 0 018 0zM3 20a6 6 0 0112 0v1H3v-1z',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round'
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const featureCards: Array<FeatureCard> = [
|
||||
{
|
||||
title: 'Painel Administrativo',
|
||||
description:
|
||||
'Acesso restrito para gerenciamento de solicitações de acesso ao sistema e outras configurações administrativas.',
|
||||
ctaLabel: 'Acessar Painel',
|
||||
href: '/(dashboard)/ti/painel-administrativo',
|
||||
palette: 'primary',
|
||||
icon: 'control'
|
||||
},
|
||||
{
|
||||
title: 'Cibersecurity SGSE',
|
||||
description:
|
||||
'Wizcard de segurança cibernética com detecção de DDoS, SQLi, APT, bloqueios automatizados, relatórios refinados e alertas sonoros/visuais.',
|
||||
ctaLabel: 'Abrir Wizcard',
|
||||
href: '/(dashboard)/ti/cibersecurity',
|
||||
palette: 'error',
|
||||
icon: 'shieldCheck',
|
||||
highlightBadges: [
|
||||
{ label: 'Layerchart', variant: 'solid' },
|
||||
{ label: 'Alertas TI', variant: 'outline' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Central de Chamados',
|
||||
description:
|
||||
'Monitore tickets, configure SLA, atribua responsáveis e acompanhe alertas de prazos.',
|
||||
ctaLabel: 'Abrir Central',
|
||||
href: '/(dashboard)/ti/central-chamados',
|
||||
palette: 'info',
|
||||
icon: 'support',
|
||||
highlightBadges: [
|
||||
{ label: 'SLA', variant: 'solid' },
|
||||
{ label: 'Alertas', variant: 'outline' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Suporte Técnico',
|
||||
description:
|
||||
'Central de atendimento para resolução de problemas técnicos e dúvidas sobre o sistema.',
|
||||
ctaLabel: 'Em breve',
|
||||
palette: 'info',
|
||||
icon: 'support',
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
title: 'Gerenciar Permissões',
|
||||
description:
|
||||
'Configure as permissões de acesso aos menus do sistema por função. Controle quem pode acessar, consultar e gravar dados.',
|
||||
ctaLabel: 'Configurar Permissões',
|
||||
href: '/(dashboard)/ti/painel-permissoes',
|
||||
palette: 'success',
|
||||
icon: 'shieldCheck'
|
||||
},
|
||||
{
|
||||
title: 'Configuração de Email',
|
||||
description:
|
||||
'Configure o servidor SMTP para envio automático de notificações e emails do sistema.',
|
||||
ctaLabel: 'Configurar SMTP',
|
||||
href: '/(dashboard)/ti/configuracoes-email',
|
||||
palette: 'secondary',
|
||||
icon: 'envelope'
|
||||
},
|
||||
{
|
||||
title: 'Monitoramento de Emails',
|
||||
description:
|
||||
'Acompanhe o status da fila de emails, identifique problemas de envio e processe manualmente quando necessário.',
|
||||
ctaLabel: 'Monitorar Emails',
|
||||
href: '/(dashboard)/ti/monitoramento-emails',
|
||||
palette: 'info',
|
||||
icon: 'envelope',
|
||||
highlightBadges: [
|
||||
{ label: 'Tempo Real', variant: 'solid' },
|
||||
{ label: 'Debug', variant: 'outline' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Gerenciar Usuários',
|
||||
description:
|
||||
'Criar, editar, bloquear e gerenciar usuários do sistema. Controle total sobre contas de acesso.',
|
||||
ctaLabel: 'Gerenciar Usuários',
|
||||
href: '/(dashboard)/ti/usuarios',
|
||||
palette: 'accent',
|
||||
icon: 'users'
|
||||
},
|
||||
{
|
||||
title: 'Solicitações de Acesso',
|
||||
description:
|
||||
'Gerencie e analise solicitações de acesso ao sistema. Aprove ou rejeite novas solicitações de forma eficiente.',
|
||||
ctaLabel: 'Gerenciar Solicitações',
|
||||
href: '/(dashboard)/ti/solicitacoes-acesso',
|
||||
palette: 'warning',
|
||||
icon: 'userPlus'
|
||||
},
|
||||
{
|
||||
title: 'Gestão de Times',
|
||||
description:
|
||||
'Organize funcionários em equipes e defina gestores. Gerencie membros e estrutura organizacional do sistema.',
|
||||
ctaLabel: 'Gerenciar Times',
|
||||
href: '/(dashboard)/ti/times',
|
||||
palette: 'success',
|
||||
icon: 'teams'
|
||||
},
|
||||
{
|
||||
title: 'Notificações e Mensagens',
|
||||
description:
|
||||
'Envie notificações para usuários do sistema via chat ou email. Configure templates de mensagens reutilizáveis.',
|
||||
ctaLabel: 'Acessar Painel',
|
||||
href: '/(dashboard)/ti/notificacoes',
|
||||
palette: 'info',
|
||||
icon: 'bell'
|
||||
},
|
||||
{
|
||||
title: 'Monitorar SGSE',
|
||||
description:
|
||||
'Monitore em tempo real as métricas técnicas do sistema e configure alertas inteligentes para a equipe de TI.',
|
||||
ctaLabel: 'Monitorar Sistema',
|
||||
href: '/(dashboard)/ti/monitoramento',
|
||||
palette: 'error',
|
||||
icon: 'monitor',
|
||||
highlightBadges: [
|
||||
{ label: 'Tempo Real', variant: 'solid' },
|
||||
{ label: 'Alertas', variant: 'outline' },
|
||||
{ label: 'Relatórios', variant: 'outline' }
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Documentação',
|
||||
description:
|
||||
'Manuais, guias e documentação técnica do sistema para usuários e administradores.',
|
||||
ctaLabel: 'Em breve',
|
||||
palette: 'primary',
|
||||
icon: 'document',
|
||||
disabled: true
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<main class="mx-auto w-full max-w-7xl space-y-12 px-4 py-10">
|
||||
<section
|
||||
class="relative overflow-hidden rounded-3xl border border-primary/25 bg-linear-to-br from-primary/10 via-base-100 to-secondary/20 p-8 shadow-2xl"
|
||||
>
|
||||
<div
|
||||
class="absolute -left-10 top-10 h-40 w-40 rounded-full bg-primary/20 blur-3xl"
|
||||
></div>
|
||||
<div
|
||||
class="absolute -bottom-16 right-0 h-56 w-56 rounded-full bg-secondary/20 blur-3xl"
|
||||
></div>
|
||||
<div
|
||||
class="relative z-10 flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between"
|
||||
>
|
||||
<div class="max-w-3xl space-y-4">
|
||||
<span
|
||||
class="inline-flex w-fit items-center gap-2 rounded-full border border-primary/40 bg-primary/10 px-4 py-1 text-xs font-semibold uppercase tracking-[0.28em] text-primary"
|
||||
>
|
||||
Tecnologia da Informação
|
||||
</span>
|
||||
<h1
|
||||
class="text-4xl font-black leading-tight text-base-content sm:text-5xl"
|
||||
>
|
||||
Sistemas de Informação
|
||||
</h1>
|
||||
<p class="text-base leading-relaxed text-base-content/70 sm:text-lg">
|
||||
Acesso restrito para gerenciamento de solicitações de acesso ao
|
||||
sistema, configuração de permissões e monitoramento técnico das
|
||||
operações do SGSE.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="grid grid-cols-2 gap-4 rounded-2xl border border-base-200/60 bg-base-100/70 p-6 shadow-lg backdrop-blur sm:max-w-sm"
|
||||
>
|
||||
<div>
|
||||
<p class="text-sm font-semibold text-base-content/60">Status</p>
|
||||
<p class="mt-2 text-2xl font-bold text-base-content">Operacional</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-semibold text-base-content/60">
|
||||
Última atualização
|
||||
</p>
|
||||
<p class="mt-2 text-xl font-bold text-base-content">Agora mesmo</p>
|
||||
</div>
|
||||
<div
|
||||
class="col-span-2 h-px bg-linear-to-r from-transparent via-base-300 to-transparent"
|
||||
></div>
|
||||
<div
|
||||
class="col-span-2 flex items-center justify-between text-sm text-base-content/70"
|
||||
>
|
||||
<span>Monitoramento em tempo real.</span>
|
||||
<span class="badge badge-primary badge-sm">SGSE</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="border-primary/25 from-primary/10 via-base-100 to-secondary/20 relative overflow-hidden rounded-3xl border bg-linear-to-br p-8 shadow-2xl"
|
||||
>
|
||||
<div class="bg-primary/20 absolute top-10 -left-10 h-40 w-40 rounded-full blur-3xl"></div>
|
||||
<div class="bg-secondary/20 absolute right-0 -bottom-16 h-56 w-56 rounded-full blur-3xl"></div>
|
||||
<div class="relative z-10 flex flex-col gap-6 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div class="max-w-3xl space-y-4">
|
||||
<span
|
||||
class="border-primary/40 bg-primary/10 text-primary inline-flex w-fit items-center gap-2 rounded-full border px-4 py-1 text-xs font-semibold tracking-[0.28em] uppercase"
|
||||
>
|
||||
Tecnologia da Informação
|
||||
</span>
|
||||
<h1 class="text-base-content text-4xl leading-tight font-black sm:text-5xl">
|
||||
Sistemas de Informação
|
||||
</h1>
|
||||
<p class="text-base-content/70 text-base leading-relaxed sm:text-lg">
|
||||
Acesso restrito para gerenciamento de solicitações de acesso ao sistema, configuração de
|
||||
permissões e monitoramento técnico das operações do SGSE.
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="border-base-200/60 bg-base-100/70 grid grid-cols-2 gap-4 rounded-2xl border p-6 shadow-lg backdrop-blur sm:max-w-sm"
|
||||
>
|
||||
<div>
|
||||
<p class="text-base-content/60 text-sm font-semibold">Status</p>
|
||||
<p class="text-base-content mt-2 text-2xl font-bold">Operacional</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="text-base-content/60 text-sm font-semibold">Última atualização</p>
|
||||
<p class="text-base-content mt-2 text-xl font-bold">Agora mesmo</p>
|
||||
</div>
|
||||
<div
|
||||
class="via-base-300 col-span-2 h-px bg-linear-to-r from-transparent to-transparent"
|
||||
></div>
|
||||
<div class="text-base-content/70 col-span-2 flex items-center justify-between text-sm">
|
||||
<span>Monitoramento em tempo real.</span>
|
||||
<span class="badge badge-primary badge-sm">SGSE</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{#each featureCards as card (card.title)}
|
||||
<article
|
||||
class={`card-hover group relative overflow-hidden rounded-2xl border ${paletteStyles[card.palette].cardBorder} bg-base-100/90 p-6 shadow-lg transition-all duration-300`}
|
||||
>
|
||||
<div
|
||||
class="absolute inset-x-6 top-0 h-24 rounded-b-full bg-linear-to-b from-base-200/40 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
></div>
|
||||
<div class="relative flex items-start gap-4">
|
||||
<div
|
||||
class={`flex h-14 w-14 items-center justify-center rounded-2xl ${paletteStyles[card.palette].iconBg} ${paletteStyles[card.palette].iconRing}`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
class={`h-7 w-7 ${paletteStyles[card.palette].iconColor}`}
|
||||
>
|
||||
{#each iconPaths[card.icon] as path (path.d)}
|
||||
<path
|
||||
d={path.d}
|
||||
stroke-linecap={path.strokeLinecap ?? "round"}
|
||||
stroke-linejoin={path.strokeLinejoin ?? "round"}
|
||||
stroke-width={path.strokeWidth ?? 2}
|
||||
/>
|
||||
{/each}
|
||||
</svg>
|
||||
</div>
|
||||
<div class="relative flex-1">
|
||||
<h2 class="text-xl font-semibold text-base-content">
|
||||
{card.title}
|
||||
</h2>
|
||||
<p class="mt-2 text-sm leading-relaxed text-base-content/70">
|
||||
{card.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<section class="grid grid-cols-1 gap-6 md:grid-cols-2 xl:grid-cols-3">
|
||||
{#each featureCards as card (card.title)}
|
||||
<article
|
||||
class={`card-hover group relative overflow-hidden rounded-2xl border ${paletteStyles[card.palette].cardBorder} bg-base-100/90 p-6 shadow-lg transition-all duration-300`}
|
||||
>
|
||||
<div
|
||||
class="from-base-200/40 absolute inset-x-6 top-0 h-24 rounded-b-full bg-linear-to-b to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
></div>
|
||||
<div class="relative flex items-start gap-4">
|
||||
<div
|
||||
class={`flex h-14 w-14 items-center justify-center rounded-2xl ${paletteStyles[card.palette].iconBg} ${paletteStyles[card.palette].iconRing}`}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
class={`h-7 w-7 ${paletteStyles[card.palette].iconColor}`}
|
||||
>
|
||||
{#each iconPaths[card.icon] as path (path.d)}
|
||||
<path
|
||||
d={path.d}
|
||||
stroke-linecap={path.strokeLinecap ?? 'round'}
|
||||
stroke-linejoin={path.strokeLinejoin ?? 'round'}
|
||||
stroke-width={path.strokeWidth ?? 2}
|
||||
/>
|
||||
{/each}
|
||||
</svg>
|
||||
</div>
|
||||
<div class="relative flex-1">
|
||||
<h2 class="text-base-content text-xl font-semibold">
|
||||
{card.title}
|
||||
</h2>
|
||||
<p class="text-base-content/70 mt-2 text-sm leading-relaxed">
|
||||
{card.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if card.highlightBadges}
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{#each card.highlightBadges as badge (badge.label)}
|
||||
{#if badge.variant === "solid"}
|
||||
<span class={`badge ${paletteStyles[card.palette].badgeSolid}`}
|
||||
>{badge.label}</span
|
||||
>
|
||||
{:else}
|
||||
<span
|
||||
class={`badge ${paletteStyles[card.palette].badgeOutline} ${paletteStyles[card.palette].iconColor}`}
|
||||
>
|
||||
{badge.label}
|
||||
</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{#if card.highlightBadges}
|
||||
<div class="mt-4 flex flex-wrap gap-2">
|
||||
{#each card.highlightBadges as badge (badge.label)}
|
||||
{#if badge.variant === 'solid'}
|
||||
<span class={`badge ${paletteStyles[card.palette].badgeSolid}`}>{badge.label}</span>
|
||||
{:else}
|
||||
<span
|
||||
class={`badge ${paletteStyles[card.palette].badgeOutline} ${paletteStyles[card.palette].iconColor}`}
|
||||
>
|
||||
{badge.label}
|
||||
</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
{#if card.href && !card.disabled}
|
||||
<a
|
||||
class={`btn ${paletteStyles[card.palette].button} btn-sm sm:btn-md shadow-md transition-all duration-200 hover:shadow-lg`}
|
||||
href={card.href}
|
||||
>
|
||||
{card.ctaLabel}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class={`btn ${paletteStyles[card.palette].button} btn-sm sm:btn-md shadow-md`}
|
||||
disabled
|
||||
>
|
||||
{card.ctaLabel}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</section>
|
||||
<div class="mt-6 flex justify-end">
|
||||
{#if card.href && !card.disabled}
|
||||
<a
|
||||
class={`btn ${paletteStyles[card.palette].button} btn-sm sm:btn-md shadow-md transition-all duration-200 hover:shadow-lg`}
|
||||
href={resolve(card.href)}
|
||||
>
|
||||
{card.ctaLabel}
|
||||
</a>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
class={`btn ${paletteStyles[card.palette].button} btn-sm sm:btn-md shadow-md`}
|
||||
disabled
|
||||
>
|
||||
{card.ctaLabel}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</article>
|
||||
{/each}
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="relative overflow-hidden rounded-2xl border border-warning/30 bg-linear-to-br from-warning/15 via-warning/10 to-warning/5 p-6 shadow-lg"
|
||||
>
|
||||
<div
|
||||
class="absolute -top-10 right-0 h-32 w-32 rounded-full bg-warning/30 blur-3xl"
|
||||
></div>
|
||||
<div class="relative z-10 flex items-start gap-4">
|
||||
<div
|
||||
class="flex h-12 w-12 items-center justify-center rounded-full bg-warning/25 text-warning"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-6 w-6 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-lg font-bold text-base-content">Área Restrita</h3>
|
||||
<p class="mt-2 text-sm leading-relaxed text-base-content/70">
|
||||
Esta área é exclusiva da equipe de Tecnologia da Informação. Garanta
|
||||
que apenas usuários autorizados acessem o Painel Administrativo e
|
||||
mantenha suas credenciais em segurança.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
class="border-warning/30 from-warning/15 via-warning/10 to-warning/5 relative overflow-hidden rounded-2xl border bg-linear-to-br p-6 shadow-lg"
|
||||
>
|
||||
<div class="bg-warning/30 absolute -top-10 right-0 h-32 w-32 rounded-full blur-3xl"></div>
|
||||
<div class="relative z-10 flex items-start gap-4">
|
||||
<div
|
||||
class="bg-warning/25 text-warning flex h-12 w-12 items-center justify-center rounded-full"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-6 w-6 stroke-current"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-base-content text-lg font-bold">Área Restrita</h3>
|
||||
<p class="text-base-content/70 mt-2 text-sm leading-relaxed">
|
||||
Esta área é exclusiva da equipe de Tecnologia da Informação. Garanta que apenas usuários
|
||||
autorizados acessem o Painel Administrativo e mantenha suas credenciais em segurança.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import CybersecurityWizcard from '$lib/components/ti/CybersecurityWizcard.svelte';
|
||||
import { resolve } from '$app/paths';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Cibersecurity SGSE • Wizcard TI</title>
|
||||
</svelte:head>
|
||||
|
||||
<section class="space-y-8 p-4 lg:p-8">
|
||||
<header class="flex flex-wrap items-center justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-semibold uppercase tracking-widest text-primary">Cibersecurity • SGSE</p>
|
||||
<h1 class="text-4xl font-black text-base-content">
|
||||
Wizcard de Segurança Avançada
|
||||
</h1>
|
||||
<p class="text-base-content/70 max-w-3xl text-sm">
|
||||
Detecta DDoS, SQLi, ataques avançados e comportamentos anômalos em tempo real.
|
||||
Permite bloquear IPs/portas, gerar relatórios refinados, configurar políticas e
|
||||
manter a operação do SGSE blindada.
|
||||
</p>
|
||||
</div>
|
||||
<a href={resolve('/ti')} class="btn btn-outline btn-primary">Voltar para TI</a>
|
||||
</header>
|
||||
|
||||
<CybersecurityWizcard />
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
BIN
cibersecurity-after-restart.png
Normal file
BIN
cibersecurity-after-restart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
BIN
cibersecurity-error-500.png
Normal file
BIN
cibersecurity-error-500.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 96 KiB |
BIN
cibersecurity-page-current.png
Normal file
BIN
cibersecurity-page-current.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
BIN
cibersecurity-working.png
Normal file
BIN
cibersecurity-working.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 276 KiB |
2
packages/backend/convex/_generated/api.d.ts
vendored
2
packages/backend/convex/_generated/api.d.ts
vendored
@@ -39,6 +39,7 @@ import type * as preferenciasNotificacao from "../preferenciasNotificacao.js";
|
||||
import type * as pushNotifications from "../pushNotifications.js";
|
||||
import type * as roles from "../roles.js";
|
||||
import type * as saldoFerias from "../saldoFerias.js";
|
||||
import type * as security from "../security.js";
|
||||
import type * as seed from "../seed.js";
|
||||
import type * as simbolos from "../simbolos.js";
|
||||
import type * as solicitacoesAcesso from "../solicitacoesAcesso.js";
|
||||
@@ -87,6 +88,7 @@ declare const fullApi: ApiFromModules<{
|
||||
pushNotifications: typeof pushNotifications;
|
||||
roles: typeof roles;
|
||||
saldoFerias: typeof saldoFerias;
|
||||
security: typeof security;
|
||||
seed: typeof seed;
|
||||
simbolos: typeof simbolos;
|
||||
solicitacoesAcesso: typeof solicitacoesAcesso;
|
||||
|
||||
@@ -32,6 +32,19 @@ crons.interval(
|
||||
{}
|
||||
);
|
||||
|
||||
crons.interval(
|
||||
"expirar-bloqueios-ip-automaticos",
|
||||
{ minutes: 5 },
|
||||
internal.security.expirarBloqueiosIpAutomaticos,
|
||||
{}
|
||||
);
|
||||
|
||||
crons.interval(
|
||||
"sincronizar-threat-intel",
|
||||
{ hours: 2 },
|
||||
internal.security.atualizarThreatIntelFeedsInternal,
|
||||
{}
|
||||
);
|
||||
|
||||
export default crons;
|
||||
|
||||
|
||||
@@ -7,6 +7,115 @@ export const simboloTipo = v.union(
|
||||
);
|
||||
export type SimboloTipo = Infer<typeof simboloTipo>;
|
||||
|
||||
export const ataqueCiberneticoTipo = 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")
|
||||
);
|
||||
export type AtaqueCiberneticoTipo = Infer<typeof ataqueCiberneticoTipo>;
|
||||
|
||||
export const severidadeSeguranca = v.union(
|
||||
v.literal("informativo"),
|
||||
v.literal("baixo"),
|
||||
v.literal("moderado"),
|
||||
v.literal("alto"),
|
||||
v.literal("critico")
|
||||
);
|
||||
export type SeveridadeSeguranca = Infer<typeof severidadeSeguranca>;
|
||||
|
||||
export const statusEventoSeguranca = v.union(
|
||||
v.literal("detectado"),
|
||||
v.literal("investigando"),
|
||||
v.literal("contido"),
|
||||
v.literal("falso_positivo"),
|
||||
v.literal("escalado"),
|
||||
v.literal("resolvido")
|
||||
);
|
||||
export type StatusEventoSeguranca = Infer<typeof statusEventoSeguranca>;
|
||||
|
||||
export const sensorSegurancaTipo = v.union(
|
||||
v.literal("network"),
|
||||
v.literal("endpoint"),
|
||||
v.literal("application"),
|
||||
v.literal("gateway"),
|
||||
v.literal("ot"),
|
||||
v.literal("honeypot")
|
||||
);
|
||||
export type SensorSegurancaTipo = Infer<typeof sensorSegurancaTipo>;
|
||||
|
||||
export const sensorSegurancaStatus = v.union(
|
||||
v.literal("ativo"),
|
||||
v.literal("inativo"),
|
||||
v.literal("degradado"),
|
||||
v.literal("manutencao")
|
||||
);
|
||||
export type SensorSegurancaStatus = Infer<typeof sensorSegurancaStatus>;
|
||||
|
||||
export const threatIntelTipo = v.union(
|
||||
v.literal("open_source"),
|
||||
v.literal("commercial"),
|
||||
v.literal("internal"),
|
||||
v.literal("gov"),
|
||||
v.literal("research")
|
||||
);
|
||||
|
||||
export const threatIntelFormato = v.union(
|
||||
v.literal("json"),
|
||||
v.literal("stix"),
|
||||
v.literal("csv"),
|
||||
v.literal("text"),
|
||||
v.literal("custom")
|
||||
);
|
||||
|
||||
export const acaoIncidenteTipo = 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")
|
||||
);
|
||||
|
||||
export const acaoIncidenteStatus = v.union(
|
||||
v.literal("pendente"),
|
||||
v.literal("executando"),
|
||||
v.literal("concluido"),
|
||||
v.literal("falhou")
|
||||
);
|
||||
|
||||
export const reportStatus = v.union(
|
||||
v.literal("pendente"),
|
||||
v.literal("processando"),
|
||||
v.literal("concluido"),
|
||||
v.literal("falhou")
|
||||
);
|
||||
|
||||
export default defineSchema({
|
||||
todos: defineTable({
|
||||
text: v.string(),
|
||||
@@ -670,7 +779,8 @@ export default defineSchema({
|
||||
v.literal("nova_mensagem"),
|
||||
v.literal("mencao"),
|
||||
v.literal("grupo_criado"),
|
||||
v.literal("adicionado_grupo")
|
||||
v.literal("adicionado_grupo"),
|
||||
v.literal("alerta_seguranca")
|
||||
),
|
||||
conversaId: v.optional(v.id("conversas")),
|
||||
mensagemId: v.optional(v.id("mensagens")),
|
||||
@@ -938,4 +1048,217 @@ export default defineSchema({
|
||||
})
|
||||
.index("by_ticket", ["ticketId", "ativo"])
|
||||
.index("by_responsavel", ["responsavelId", "ativo"]),
|
||||
|
||||
// Sistema de Segurança Cibernética
|
||||
networkSensors: defineTable({
|
||||
nome: v.string(),
|
||||
tipo: sensorSegurancaTipo,
|
||||
status: sensorSegurancaStatus,
|
||||
escopo: v.optional(v.string()),
|
||||
ipMonitorado: v.optional(v.string()),
|
||||
hostname: v.optional(v.string()),
|
||||
regioes: v.optional(v.array(v.string())),
|
||||
portasMonitoradas: v.optional(v.array(v.number())),
|
||||
protocolos: v.optional(v.array(v.string())),
|
||||
capacidades: v.optional(v.array(v.string())),
|
||||
ultimaSincronizacao: v.number(),
|
||||
ultimoHeartbeat: v.optional(v.number()),
|
||||
latenciaMs: v.optional(v.number()),
|
||||
errosConsecutivos: v.optional(v.number()),
|
||||
agenteVersao: v.optional(v.string()),
|
||||
notas: v.optional(v.string()),
|
||||
})
|
||||
.index("by_tipo", ["tipo"])
|
||||
.index("by_status", ["status"])
|
||||
.index("by_hostname", ["hostname"]),
|
||||
|
||||
ipReputation: defineTable({
|
||||
indicador: v.string(),
|
||||
categoria: v.union(
|
||||
v.literal("ip"),
|
||||
v.literal("dominio"),
|
||||
v.literal("hash"),
|
||||
v.literal("email")
|
||||
),
|
||||
reputacao: v.number(), // -100 (malicioso) até 100 (confiável)
|
||||
severidadeMax: severidadeSeguranca,
|
||||
whitelist: v.boolean(),
|
||||
blacklist: v.boolean(),
|
||||
ocorrencias: v.number(),
|
||||
primeiroRegistro: v.number(),
|
||||
ultimoRegistro: v.number(),
|
||||
bloqueadoAte: v.optional(v.number()),
|
||||
origem: v.optional(v.string()),
|
||||
comentarios: v.optional(v.string()),
|
||||
classificacoes: v.optional(v.array(v.string())),
|
||||
ultimaAcaoId: v.optional(v.id("incidentActions")),
|
||||
})
|
||||
.index("by_indicador", ["indicador"])
|
||||
.index("by_reputacao", ["reputacao"])
|
||||
.index("by_blacklist", ["blacklist"])
|
||||
.index("by_whitelist", ["whitelist"]),
|
||||
|
||||
portRules: defineTable({
|
||||
porta: v.number(),
|
||||
protocolo: v.union(
|
||||
v.literal("tcp"),
|
||||
v.literal("udp"),
|
||||
v.literal("icmp"),
|
||||
v.literal("quic"),
|
||||
v.literal("any")
|
||||
),
|
||||
acao: v.union(
|
||||
v.literal("permitir"),
|
||||
v.literal("bloquear"),
|
||||
v.literal("monitorar"),
|
||||
v.literal("rate_limit")
|
||||
),
|
||||
temporario: v.boolean(),
|
||||
severidadeMin: severidadeSeguranca,
|
||||
duracaoSegundos: v.optional(v.number()),
|
||||
expiraEm: v.optional(v.number()),
|
||||
criadoPor: v.id("usuarios"),
|
||||
atualizadoPor: v.optional(v.id("usuarios")),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number(),
|
||||
notas: v.optional(v.string()),
|
||||
tags: v.optional(v.array(v.string())),
|
||||
listaReferencia: v.optional(v.id("ipReputation")),
|
||||
})
|
||||
.index("by_porta_protocolo", ["porta", "protocolo"])
|
||||
.index("by_acao", ["acao"])
|
||||
.index("by_expiracao", ["expiraEm"]),
|
||||
|
||||
threatIntelFeeds: defineTable({
|
||||
nomeFonte: v.string(),
|
||||
tipo: threatIntelTipo,
|
||||
formato: threatIntelFormato,
|
||||
url: v.optional(v.string()),
|
||||
ativo: v.boolean(),
|
||||
prioridade: v.union(
|
||||
v.literal("baixa"),
|
||||
v.literal("media"),
|
||||
v.literal("alta"),
|
||||
v.literal("critica")
|
||||
),
|
||||
ultimaSincronizacao: v.optional(v.number()),
|
||||
entradasProcessadas: v.optional(v.number()),
|
||||
errosConsecutivos: v.optional(v.number()),
|
||||
autenticacaoNecessaria: v.optional(v.boolean()),
|
||||
configuracao: v.optional(
|
||||
v.object({
|
||||
tokenId: v.optional(v.id("_storage")),
|
||||
escopo: v.optional(v.string()),
|
||||
})
|
||||
),
|
||||
criadoPor: v.id("usuarios"),
|
||||
atualizadoPor: v.optional(v.id("usuarios")),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number(),
|
||||
})
|
||||
.index("by_tipo", ["tipo"])
|
||||
.index("by_ativo", ["ativo"])
|
||||
.index("by_prioridade", ["prioridade"]),
|
||||
|
||||
securityEvents: defineTable({
|
||||
referencia: v.string(),
|
||||
timestamp: v.number(),
|
||||
tipoAtaque: ataqueCiberneticoTipo,
|
||||
severidade: severidadeSeguranca,
|
||||
status: statusEventoSeguranca,
|
||||
descricao: v.string(),
|
||||
origemIp: v.optional(v.string()),
|
||||
origemRegiao: v.optional(v.string()),
|
||||
origemAsn: v.optional(v.string()),
|
||||
destinoIp: v.optional(v.string()),
|
||||
destinoPorta: v.optional(v.number()),
|
||||
protocolo: v.optional(v.string()),
|
||||
transporte: v.optional(v.string()),
|
||||
sensorId: v.optional(v.id("networkSensors")),
|
||||
detectadoPor: v.optional(v.string()),
|
||||
mitreTechnique: 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()),
|
||||
})
|
||||
),
|
||||
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()),
|
||||
})
|
||||
),
|
||||
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()),
|
||||
})
|
||||
),
|
||||
correlacoes: v.optional(v.array(v.id("securityEvents"))),
|
||||
referenciasExternas: v.optional(v.array(v.string())),
|
||||
tags: v.optional(v.array(v.string())),
|
||||
criadoPor: v.optional(v.id("usuarios")),
|
||||
atualizadoEm: v.number(),
|
||||
})
|
||||
.index("by_referencia", ["referencia"])
|
||||
.index("by_timestamp", ["timestamp"])
|
||||
.index("by_tipo", ["tipoAtaque", "timestamp"])
|
||||
.index("by_severidade", ["severidade", "timestamp"])
|
||||
.index("by_status", ["status", "timestamp"]),
|
||||
|
||||
incidentActions: defineTable({
|
||||
eventoId: v.id("securityEvents"),
|
||||
tipo: acaoIncidenteTipo,
|
||||
origem: v.union(v.literal("automatico"), v.literal("manual")),
|
||||
status: acaoIncidenteStatus,
|
||||
executadoPor: v.optional(v.id("usuarios")),
|
||||
detalhes: v.optional(v.string()),
|
||||
resultado: v.optional(v.string()),
|
||||
relacionadoA: v.optional(v.id("ipReputation")),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number(),
|
||||
})
|
||||
.index("by_evento", ["eventoId", "status"])
|
||||
.index("by_tipo", ["tipo", "status"]),
|
||||
|
||||
reportRequests: defineTable({
|
||||
solicitanteId: v.id("usuarios"),
|
||||
filtros: v.object({
|
||||
dataInicio: v.number(),
|
||||
dataFim: v.number(),
|
||||
severidades: v.optional(v.array(severidadeSeguranca)),
|
||||
tiposAtaque: v.optional(v.array(ataqueCiberneticoTipo)),
|
||||
incluirIndicadores: v.optional(v.boolean()),
|
||||
incluirMetricas: v.optional(v.boolean()),
|
||||
incluirAcoes: v.optional(v.boolean()),
|
||||
}),
|
||||
status: reportStatus,
|
||||
resultadoId: v.optional(v.id("_storage")),
|
||||
observacoes: v.optional(v.string()),
|
||||
criadoEm: v.number(),
|
||||
atualizadoEm: v.number(),
|
||||
concluidoEm: v.optional(v.number()),
|
||||
erro: v.optional(v.string()),
|
||||
})
|
||||
.index("by_status", ["status"])
|
||||
.index("by_solicitante", ["solicitanteId", "status"])
|
||||
.index("by_criado_em", ["criadoEm"]),
|
||||
});
|
||||
|
||||
1225
packages/backend/convex/security.ts
Normal file
1225
packages/backend/convex/security.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user