|
|
|
@@ -58,14 +58,20 @@
|
|
|
|
return Array.from(set).slice(0, 16);
|
|
|
|
return Array.from(set).slice(0, 16);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
function adicionarEmailSugestao(email: string) {
|
|
|
|
function adicionarEmailSugestao(email: string) {
|
|
|
|
const linhas = alertEmails.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
|
|
const linhas = alertEmails
|
|
|
|
|
|
|
|
.split('\n')
|
|
|
|
|
|
|
|
.map((s) => s.trim())
|
|
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
if (!linhas.includes(email)) {
|
|
|
|
if (!linhas.includes(email)) {
|
|
|
|
linhas.push(email);
|
|
|
|
linhas.push(email);
|
|
|
|
alertEmails = linhas.join('\n');
|
|
|
|
alertEmails = linhas.join('\n');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
function adicionarChatSugestao(user: string) {
|
|
|
|
function adicionarChatSugestao(user: string) {
|
|
|
|
const linhas = alertUsersChat.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
|
|
const linhas = alertUsersChat
|
|
|
|
|
|
|
|
.split('\n')
|
|
|
|
|
|
|
|
.map((s) => s.trim())
|
|
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
if (!linhas.includes(user)) {
|
|
|
|
if (!linhas.includes(user)) {
|
|
|
|
linhas.push(user);
|
|
|
|
linhas.push(user);
|
|
|
|
alertUsersChat = linhas.join('\n');
|
|
|
|
alertUsersChat = linhas.join('\n');
|
|
|
|
@@ -88,7 +94,7 @@
|
|
|
|
emails: parseLinhasParaArray(alertEmails),
|
|
|
|
emails: parseLinhasParaArray(alertEmails),
|
|
|
|
chatUsers: parseLinhasParaArray(alertUsersChat),
|
|
|
|
chatUsers: parseLinhasParaArray(alertUsersChat),
|
|
|
|
severidadeMin: alertSeveridadeMin,
|
|
|
|
severidadeMin: alertSeveridadeMin,
|
|
|
|
tiposAtaque: (alertTiposAtaque as AtaqueCiberneticoTipo[]),
|
|
|
|
tiposAtaque: alertTiposAtaque as AtaqueCiberneticoTipo[],
|
|
|
|
reenvioMin: alertReenvioMin,
|
|
|
|
reenvioMin: alertReenvioMin,
|
|
|
|
criadoPor: obterUsuarioId()
|
|
|
|
criadoPor: obterUsuarioId()
|
|
|
|
});
|
|
|
|
});
|
|
|
|
@@ -257,7 +263,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
// Efeito: observar chegada de novos eventos e acionar toast/contador
|
|
|
|
// Efeito: observar chegada de novos eventos e acionar toast/contador
|
|
|
|
$effect(() => {
|
|
|
|
$effect(() => {
|
|
|
|
const lista = (eventos?.data ?? []);
|
|
|
|
const lista = eventos?.data ?? [];
|
|
|
|
if (!lista.length) return;
|
|
|
|
if (!lista.length) return;
|
|
|
|
// conta apenas eventos com timestamp maior que o último visto
|
|
|
|
// conta apenas eventos com timestamp maior que o último visto
|
|
|
|
const novos = lista.filter((e) => e.timestamp > ultimoTsVisto).length;
|
|
|
|
const novos = lista.filter((e) => e.timestamp > ultimoTsVisto).length;
|
|
|
|
@@ -316,10 +322,7 @@
|
|
|
|
return buckets;
|
|
|
|
return buckets;
|
|
|
|
});
|
|
|
|
});
|
|
|
|
const maxBucketTotal = $derived.by(() =>
|
|
|
|
const maxBucketTotal = $derived.by(() =>
|
|
|
|
Math.max(
|
|
|
|
Math.max(1, ...timelineBuckets.map((b) => Object.values(b.counts).reduce((a, n) => a + n, 0)))
|
|
|
|
1,
|
|
|
|
|
|
|
|
...timelineBuckets.map((b) => Object.values(b.counts).reduce((a, n) => a + n, 0))
|
|
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
);
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
function maxSeriesValue(dataset: Array<Array<number>>): number {
|
|
|
|
function maxSeriesValue(dataset: Array<Array<number>>): number {
|
|
|
|
@@ -517,7 +520,7 @@
|
|
|
|
if (!win) return;
|
|
|
|
if (!win) return;
|
|
|
|
let conteudo: string;
|
|
|
|
let conteudo: string;
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const data = r.observacoes ? JSON.parse(r.observacoes) as any : null;
|
|
|
|
const data = r.observacoes ? (JSON.parse(r.observacoes) as any) : null;
|
|
|
|
const total = data?.total ?? '—';
|
|
|
|
const total = data?.total ?? '—';
|
|
|
|
const porSeveridade = data?.porSeveridade ?? {};
|
|
|
|
const porSeveridade = data?.porSeveridade ?? {};
|
|
|
|
const porAtaque = data?.porAtaque ?? {};
|
|
|
|
const porAtaque = data?.porAtaque ?? {};
|
|
|
|
@@ -528,7 +531,9 @@
|
|
|
|
.map(([k, v]) => `<tr><td>${k}</td><td style="text-align:right">${v as number}</td></tr>`)
|
|
|
|
.map(([k, v]) => `<tr><td>${k}</td><td style="text-align:right">${v as number}</td></tr>`)
|
|
|
|
.join('');
|
|
|
|
.join('');
|
|
|
|
const criadoStr = new Date(r.criadoEm).toLocaleString('pt-BR', { hour12: false });
|
|
|
|
const criadoStr = new Date(r.criadoEm).toLocaleString('pt-BR', { hour12: false });
|
|
|
|
const concluidoStr = r.concluidoEm ? new Date(r.concluidoEm).toLocaleString('pt-BR', { hour12: false }) : '—';
|
|
|
|
const concluidoStr = r.concluidoEm
|
|
|
|
|
|
|
|
? new Date(r.concluidoEm).toLocaleString('pt-BR', { hour12: false })
|
|
|
|
|
|
|
|
: '—';
|
|
|
|
const agoraStr = new Date().toLocaleString('pt-BR', { hour12: false });
|
|
|
|
const agoraStr = new Date().toLocaleString('pt-BR', { hour12: false });
|
|
|
|
conteudo =
|
|
|
|
conteudo =
|
|
|
|
'<!doctype html>' +
|
|
|
|
'<!doctype html>' +
|
|
|
|
@@ -547,15 +552,25 @@
|
|
|
|
'</style></head><body>' +
|
|
|
|
'</style></head><body>' +
|
|
|
|
'<div class="no-print" style="text-align:right;margin-bottom:12px"><button onclick="window.print()">Imprimir</button></div>' +
|
|
|
|
'<div class="no-print" style="text-align:right;margin-bottom:12px"><button onclick="window.print()">Imprimir</button></div>' +
|
|
|
|
'<h1>Relatório de Segurança</h1>' +
|
|
|
|
'<h1>Relatório de Segurança</h1>' +
|
|
|
|
'<div class="meta">Status: <strong>' + r.status + '</strong> · Criado: ' + criadoStr + ' · Concluído: ' + concluidoStr + '</div>' +
|
|
|
|
'<div class="meta">Status: <strong>' +
|
|
|
|
|
|
|
|
r.status +
|
|
|
|
|
|
|
|
'</strong> · Criado: ' +
|
|
|
|
|
|
|
|
criadoStr +
|
|
|
|
|
|
|
|
' · Concluído: ' +
|
|
|
|
|
|
|
|
concluidoStr +
|
|
|
|
|
|
|
|
'</div>' +
|
|
|
|
'<h2>Resumo</h2>' +
|
|
|
|
'<h2>Resumo</h2>' +
|
|
|
|
'<p>Total de eventos no período: <strong>' + total + '</strong></p>' +
|
|
|
|
'<p>Total de eventos no período: <strong>' +
|
|
|
|
|
|
|
|
total +
|
|
|
|
|
|
|
|
'</strong></p>' +
|
|
|
|
'<div class="grid"><div><h2>Por Severidade</h2><table><thead><tr><th>Severidade</th><th style="text-align:right">Qtde</th></tr></thead><tbody>' +
|
|
|
|
'<div class="grid"><div><h2>Por Severidade</h2><table><thead><tr><th>Severidade</th><th style="text-align:right">Qtde</th></tr></thead><tbody>' +
|
|
|
|
(linhasSev || '<tr><td colspan="2">—</td></tr>') +
|
|
|
|
(linhasSev || '<tr><td colspan="2">—</td></tr>') +
|
|
|
|
'</tbody></table></div><div><h2>Por Tipo de Ataque</h2><table><thead><tr><th>Tipo</th><th style="text-align:right">Qtde</th></tr></thead><tbody>' +
|
|
|
|
'</tbody></table></div><div><h2>Por Tipo de Ataque</h2><table><thead><tr><th>Tipo</th><th style="text-align:right">Qtde</th></tr></thead><tbody>' +
|
|
|
|
(linhasAtk || '<tr><td colspan="2">—</td></tr>') +
|
|
|
|
(linhasAtk || '<tr><td colspan="2">—</td></tr>') +
|
|
|
|
'</tbody></table></div></div>' +
|
|
|
|
'</tbody></table></div></div>' +
|
|
|
|
'<div class="footer">Gerado por SGSE · ' + agoraStr + '</div>' +
|
|
|
|
'<div class="footer">Gerado por SGSE · ' +
|
|
|
|
|
|
|
|
agoraStr +
|
|
|
|
|
|
|
|
'</div>' +
|
|
|
|
'</body></html>';
|
|
|
|
'</body></html>';
|
|
|
|
} catch {
|
|
|
|
} catch {
|
|
|
|
const obs = (r.observacoes ?? '').replace(/</g, '<');
|
|
|
|
const obs = (r.observacoes ?? '').replace(/</g, '<');
|
|
|
|
@@ -565,8 +580,12 @@
|
|
|
|
conteudo =
|
|
|
|
conteudo =
|
|
|
|
'<!doctype html><meta charset="utf-8"/>' +
|
|
|
|
'<!doctype html><meta charset="utf-8"/>' +
|
|
|
|
'<title>Relatório</title>' +
|
|
|
|
'<title>Relatório</title>' +
|
|
|
|
'<pre>' + obs + '</pre>' +
|
|
|
|
'<pre>' +
|
|
|
|
scriptOpen + 'window.print()' + scriptClose;
|
|
|
|
obs +
|
|
|
|
|
|
|
|
'</pre>' +
|
|
|
|
|
|
|
|
scriptOpen +
|
|
|
|
|
|
|
|
'window.print()' +
|
|
|
|
|
|
|
|
scriptClose;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
win.document.open();
|
|
|
|
win.document.open();
|
|
|
|
win.document.write(conteudo);
|
|
|
|
win.document.write(conteudo);
|
|
|
|
@@ -763,167 +782,20 @@
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Alertas e Notificações (rodapé) -->
|
|
|
|
<!-- (Seção Alertas e Notificações movida para depois de Rate Limiting Avançado) -->
|
|
|
|
<section class="border-info/20 bg-base-100/80 mt-6 rounded-3xl border p-6 shadow-2xl">
|
|
|
|
|
|
|
|
<h3 class="text-info text-2xl font-bold">Alertas e Notificações</h3>
|
|
|
|
|
|
|
|
<p class="text-base-content/70 mt-1 text-sm">Configure destinatários, níveis e tipos de alarme e reenvio.</p>
|
|
|
|
|
|
|
|
<div class="mt-4 grid gap-6 md:grid-cols-2">
|
|
|
|
|
|
|
|
<!-- Emails -->
|
|
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
|
|
|
<h4 class="text-base-content font-semibold">Emails de destino</h4>
|
|
|
|
|
|
|
|
<textarea class="textarea textarea-bordered w-full" rows="4" bind:value={alertEmails} placeholder="Um email por linha"></textarea>
|
|
|
|
|
|
|
|
{#if sugestoesEmails.length}
|
|
|
|
|
|
|
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
|
|
|
|
|
|
{#each sugestoesEmails as s (s)}
|
|
|
|
|
|
|
|
<button type="button" class="badge badge-outline" onclick={() => adicionarEmailSugestao(s)}>{s}</button>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
<div class="grid gap-3 md:grid-cols-2">
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Severidade mínima</span>
|
|
|
|
|
|
|
|
<select class="select select-bordered select-sm" bind:value={alertSeveridadeMin}>
|
|
|
|
|
|
|
|
{#each severidadesDisponiveis as s (s)}
|
|
|
|
|
|
|
|
<option value={s}>{severityLabels[s]}</option>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Tipos de ataque</span>
|
|
|
|
|
|
|
|
<select class="select select-bordered select-sm" multiple size="5" bind:value={alertTiposAtaque}>
|
|
|
|
|
|
|
|
{#each Object.keys(attackLabels) as t (t)}
|
|
|
|
|
|
|
|
<option value={t}>{attackLabels[t as AtaqueCiberneticoTipo]}</option>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
|
|
|
<label class="flex items-center gap-2 text-xs">
|
|
|
|
|
|
|
|
<input type="checkbox" class="checkbox checkbox-xs" bind:checked={enviarPorEmail} />
|
|
|
|
|
|
|
|
<span>Enviar por Email</span>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label class="flex items-center gap-2 text-xs">
|
|
|
|
|
|
|
|
<input type="checkbox" class="checkbox checkbox-xs" bind:checked={enviarPorChat} />
|
|
|
|
|
|
|
|
<span>Enviar por Chat</span>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Tempo de reenvio (minutos)</span>
|
|
|
|
|
|
|
|
<input type="number" min="1" max="1440" class="input input-bordered input-sm" bind:value={alertReenvioMin} />
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Template de email</span>
|
|
|
|
|
|
|
|
<select class="select select-bordered select-sm" bind:value={alertTemplate}>
|
|
|
|
|
|
|
|
<option value="incidente_critico">Incidente Crítico - Ação Imediata</option>
|
|
|
|
|
|
|
|
<option value="bloqueio_automatico">Bloqueio Automático</option>
|
|
|
|
|
|
|
|
<option value="sumario_30min">Sumário 30 Min</option>
|
|
|
|
|
|
|
|
<option value="anormalidade">Anomalia Detectada</option>
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<button type="button" class="btn btn-info btn-sm" onclick={() => salvarPreferenciasAlertas(editarAlertConfigId ?? undefined)}>
|
|
|
|
|
|
|
|
{editarAlertConfigId ? 'Atualizar preferências' : 'Salvar preferências'}
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Chat -->
|
|
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
|
|
|
<h4 class="text-base-content font-semibold">Alertas por Chat</h4>
|
|
|
|
|
|
|
|
<textarea class="textarea textarea-bordered w-full" rows="3" bind:value={alertUsersChat} placeholder="IDs de usuários de chat (um por linha)"></textarea>
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Selecionar usuários de chat</span>
|
|
|
|
|
|
|
|
<select
|
|
|
|
|
|
|
|
class="select select-bordered select-sm"
|
|
|
|
|
|
|
|
multiple
|
|
|
|
|
|
|
|
size="6"
|
|
|
|
|
|
|
|
onchange={(e) => {
|
|
|
|
|
|
|
|
const options = Array.from((e.target as HTMLSelectElement).selectedOptions).map((o) => o.value);
|
|
|
|
|
|
|
|
for (const val of options) adicionarChatSugestao(val);
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{#each (usuariosParaChat?.data ?? []) as u (u._id)}
|
|
|
|
|
|
|
|
<option value={u.username ?? u.email}>{u.nome} ({u.username ?? u.email})</option>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
{#if sugestoesChatUsers.length}
|
|
|
|
|
|
|
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
|
|
|
|
|
|
{#each sugestoesChatUsers as s (s)}
|
|
|
|
|
|
|
|
<button type="button" class="badge badge-outline" onclick={() => adicionarChatSugestao(s)}>{s}</button>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
<div class="grid gap-3 md:grid-cols-2">
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Severidade mínima</span>
|
|
|
|
|
|
|
|
<select class="select select-bordered select-sm" bind:value={chatSeveridadeMin}>
|
|
|
|
|
|
|
|
{#each severidadesDisponiveis as s (s)}
|
|
|
|
|
|
|
|
<option value={s}>{severityLabels[s]}</option>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Tipos de ataque</span>
|
|
|
|
|
|
|
|
<select class="select select-bordered select-sm" multiple size="5" bind:value={chatTiposAtaque}>
|
|
|
|
|
|
|
|
{#each Object.keys(attackLabels) as t (t)}
|
|
|
|
|
|
|
|
<option value={t}>{attackLabels[t as AtaqueCiberneticoTipo]}</option>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Tempo de reenvio (minutos)</span>
|
|
|
|
|
|
|
|
<input type="number" min="1" max="1440" class="input input-bordered input-sm" bind:value={chatReenvioMin} />
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<button type="button" class="btn btn-outline btn-sm" onclick={() => salvarPreferenciasAlertas(editarAlertConfigId ?? undefined)}>
|
|
|
|
|
|
|
|
Salvar preferências de chat
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="divider my-6"></div>
|
|
|
|
|
|
|
|
<h4 class="text-base-content text-sm font-semibold">Configurações salvas</h4>
|
|
|
|
|
|
|
|
{#if alertConfigs?.data?.length}
|
|
|
|
|
|
|
|
<div class="mt-2 grid gap-3 md:grid-cols-2">
|
|
|
|
|
|
|
|
{#each alertConfigs.data as cfg (cfg._id)}
|
|
|
|
|
|
|
|
<div class="border-base-200 rounded-xl border p-3">
|
|
|
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
|
|
|
<p class="font-semibold">{cfg.nome}</p>
|
|
|
|
|
|
|
|
<p class="text-base-content/60 text-xs">
|
|
|
|
|
|
|
|
Canais: {cfg.canais.email ? 'Email' : ''}{cfg.canais.email && cfg.canais.chat ? ' + ' : ''}{cfg.canais.chat ? 'Chat' : ''}
|
|
|
|
|
|
|
|
• Sev. mínima: {severityLabels[cfg.severidadeMin]}
|
|
|
|
|
|
|
|
• Reenvio: {cfg.reenvioMin} min
|
|
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
{#if cfg.emails.length}
|
|
|
|
|
|
|
|
<p class="text-base-content/60 text-xs">Emails: {cfg.emails.join(', ')}</p>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
{#if cfg.chatUsers.length}
|
|
|
|
|
|
|
|
<p class="text-base-content/60 text-xs">Chat: {cfg.chatUsers.join(', ')}</p>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
|
|
|
<button class="btn btn-xs" type="button" onclick={() => carregarParaEdicao(cfg)}>Editar</button>
|
|
|
|
|
|
|
|
<button class="btn btn-xs btn-error" type="button" onclick={() => excluirPreferenciasAlertas(cfg._id)}>Excluir</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{:else}
|
|
|
|
|
|
|
|
<p class="text-base-content/60 mt-2 text-xs">Nenhuma configuração salva.</p>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
<!-- (Seção Alertas e Notificações removida desta posição; renderizada ao final) -->
|
|
|
|
|
|
|
|
<section class="grid gap-6 lg:grid-cols-3">
|
|
|
|
<section class="grid gap-6 lg:grid-cols-3">
|
|
|
|
<div class="border-primary/20 bg-base-100/80 rounded-3xl border p-6 shadow-2xl lg:col-span-2">
|
|
|
|
<div class="border-primary/20 bg-base-100/80 rounded-3xl border p-6 shadow-2xl lg:col-span-2">
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<h2 class="text-primary text-2xl font-bold">Layerchart Threat Matrix</h2>
|
|
|
|
<h2 class="text-primary text-2xl font-bold">Threat Matrix</h2>
|
|
|
|
<p class="text-base-content/70 text-sm">
|
|
|
|
<p class="text-base-content/70 text-sm">
|
|
|
|
Correlação temporal entre DDoS, SQLi, ataques avançados e bloqueios automáticos.
|
|
|
|
Correlação temporal entre DDoS, SQLi, ataques avançados e bloqueios automáticos.
|
|
|
|
</p>
|
|
|
|
</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{#if health?.data}
|
|
|
|
{#if health?.data}
|
|
|
|
<div class="badge badge-outline">
|
|
|
|
<div class="badge badge-outline">
|
|
|
|
<span class="mr-2 h-2 w-2 rounded-full {health.data.ok ? 'bg-success' : 'bg-error'}"></span>
|
|
|
|
<span class="mr-2 h-2 w-2 rounded-full {health.data.ok ? 'bg-success' : 'bg-error'}"
|
|
|
|
|
|
|
|
></span>
|
|
|
|
{health.data.ok ? 'Saúde OK' : 'Instável'} · Pendentes: {health.data.pendingReports}
|
|
|
|
{health.data.ok ? 'Saúde OK' : 'Instável'} · Pendentes: {health.data.pendingReports}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{/if}
|
|
|
|
{/if}
|
|
|
|
@@ -997,17 +869,30 @@
|
|
|
|
<div class="mt-3 grid grid-cols-12 gap-2">
|
|
|
|
<div class="mt-3 grid grid-cols-12 gap-2">
|
|
|
|
{#each timelineBuckets as b}
|
|
|
|
{#each timelineBuckets as b}
|
|
|
|
<div class="col-span-1">
|
|
|
|
<div class="col-span-1">
|
|
|
|
<div class="h-24 w-full overflow-hidden rounded bg-base-300">
|
|
|
|
<div class="bg-base-300 h-24 w-full overflow-hidden rounded">
|
|
|
|
{#each tiposParaChart as t}
|
|
|
|
{#each tiposParaChart as t}
|
|
|
|
{#if b.counts[t] > 0}
|
|
|
|
{#if b.counts[t] > 0}
|
|
|
|
<div class="w-full"
|
|
|
|
<div
|
|
|
|
style={`height:${(b.counts[t] / Math.max(1, Object.values(b.counts).reduce((a, n) => a + n, 0))) * 100}%; background:${coresTipo[t]}`}
|
|
|
|
class="w-full"
|
|
|
|
title={`${t}: ${b.counts[t]}`}></div>
|
|
|
|
style={`height:${
|
|
|
|
|
|
|
|
(b.counts[t] /
|
|
|
|
|
|
|
|
Math.max(
|
|
|
|
|
|
|
|
1,
|
|
|
|
|
|
|
|
Object.values(b.counts).reduce((a, n) => a + n, 0)
|
|
|
|
|
|
|
|
)) *
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
}%; background:${coresTipo[t]}`}
|
|
|
|
|
|
|
|
title={`${t}: ${b.counts[t]}`}
|
|
|
|
|
|
|
|
></div>
|
|
|
|
{/if}
|
|
|
|
{/if}
|
|
|
|
{/each}
|
|
|
|
{/each}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="mt-1 text-center text-[10px] leading-tight">
|
|
|
|
<div class="mt-1 text-center text-[10px] leading-tight">
|
|
|
|
{new Date(b.inicio).toLocaleTimeString('pt-BR', { hour12: false, hour: '2-digit', minute: '2-digit' })}
|
|
|
|
{new Date(b.inicio).toLocaleTimeString('pt-BR', {
|
|
|
|
|
|
|
|
hour12: false,
|
|
|
|
|
|
|
|
hour: '2-digit',
|
|
|
|
|
|
|
|
minute: '2-digit'
|
|
|
|
|
|
|
|
})}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
{/each}
|
|
|
|
{/each}
|
|
|
|
@@ -1020,7 +905,7 @@
|
|
|
|
<!-- Top destinos (IP/protocolo) no período -->
|
|
|
|
<!-- Top destinos (IP/protocolo) no período -->
|
|
|
|
<div class="mt-4">
|
|
|
|
<div class="mt-4">
|
|
|
|
<h4 class="text-sm font-semibold">Top destinos (IP · protocolo)</h4>
|
|
|
|
<h4 class="text-sm font-semibold">Top destinos (IP · protocolo)</h4>
|
|
|
|
<table class="table table-zebra mt-2 w-full text-xs">
|
|
|
|
<table class="table-zebra mt-2 table w-full text-xs">
|
|
|
|
<thead>
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<tr>
|
|
|
|
<th>Destino</th>
|
|
|
|
<th>Destino</th>
|
|
|
|
@@ -1029,12 +914,10 @@
|
|
|
|
</tr>
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
</thead>
|
|
|
|
<tbody>
|
|
|
|
<tbody>
|
|
|
|
{#each [...new Map(
|
|
|
|
{#each [...new Map(timelineBuckets
|
|
|
|
timelineBuckets
|
|
|
|
.map((b) => Object.entries(b.topDestinos))
|
|
|
|
.map((b) => Object.entries(b.topDestinos))
|
|
|
|
.flat()
|
|
|
|
.flat()
|
|
|
|
.reduce((acc, [k, v]) => acc.set(k, (acc.get(k) ?? 0) + (v as number)), new Map()))].slice(0, 8) as item}
|
|
|
|
.reduce((acc, [k, v]) => acc.set(k, (acc.get(k) ?? 0) + (v as number)), new Map())
|
|
|
|
|
|
|
|
)].slice(0, 8) as item}
|
|
|
|
|
|
|
|
{#key item[0]}
|
|
|
|
{#key item[0]}
|
|
|
|
{@const partes = item[0].split('|')}
|
|
|
|
{@const partes = item[0].split('|')}
|
|
|
|
<tr>
|
|
|
|
<tr>
|
|
|
|
@@ -1193,7 +1076,9 @@
|
|
|
|
class="btn btn-xs btn-warning"
|
|
|
|
class="btn btn-xs btn-warning"
|
|
|
|
onclick={async () => {
|
|
|
|
onclick={async () => {
|
|
|
|
try {
|
|
|
|
try {
|
|
|
|
const resultado = await client.mutation(api.security.criarEventosTeste, { quantidade: 10 });
|
|
|
|
const resultado = await client.mutation(api.security.criarEventosTeste, {
|
|
|
|
|
|
|
|
quantidade: 10
|
|
|
|
|
|
|
|
});
|
|
|
|
feedback = {
|
|
|
|
feedback = {
|
|
|
|
tipo: 'success',
|
|
|
|
tipo: 'success',
|
|
|
|
mensagem: `✅ ${resultado.eventosCriados} eventos de teste criados com sucesso!`
|
|
|
|
mensagem: `✅ ${resultado.eventosCriados} eventos de teste criados com sucesso!`
|
|
|
|
@@ -1241,7 +1126,9 @@
|
|
|
|
{/each}
|
|
|
|
{/each}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class={`mt-4 space-y-4 ${eventosFiltrados.length > 6 ? 'max-h-[28rem] overflow-auto pr-2' : ''}`}>
|
|
|
|
<div
|
|
|
|
|
|
|
|
class={`mt-4 space-y-4 ${eventosFiltrados.length > 6 ? 'max-h-[28rem] overflow-auto pr-2' : ''}`}
|
|
|
|
|
|
|
|
>
|
|
|
|
{#if eventosFiltrados.length === 0}
|
|
|
|
{#if eventosFiltrados.length === 0}
|
|
|
|
<p class="text-base-content/60 text-sm">Nenhum evento correspondente aos filtros.</p>
|
|
|
|
<p class="text-base-content/60 text-sm">Nenhum evento correspondente aos filtros.</p>
|
|
|
|
{:else}
|
|
|
|
{:else}
|
|
|
|
@@ -1335,7 +1222,11 @@
|
|
|
|
<div class="border-accent/20 bg-base-100 space-y-6 rounded-3xl border p-6 shadow-2xl">
|
|
|
|
<div class="border-accent/20 bg-base-100 space-y-6 rounded-3xl border p-6 shadow-2xl">
|
|
|
|
<div>
|
|
|
|
<div>
|
|
|
|
<h4 class="text-accent text-lg font-bold">Lista Negra Inteligente</h4>
|
|
|
|
<h4 class="text-accent text-lg font-bold">Lista Negra Inteligente</h4>
|
|
|
|
<ul class="mt-4 space-y-3 text-sm {ipCriticos.length >= 10 ? 'max-h-64 overflow-auto pr-2' : ''}">
|
|
|
|
<ul
|
|
|
|
|
|
|
|
class="mt-4 space-y-3 text-sm {ipCriticos.length >= 10
|
|
|
|
|
|
|
|
? 'max-h-64 overflow-auto pr-2'
|
|
|
|
|
|
|
|
: ''}"
|
|
|
|
|
|
|
|
>
|
|
|
|
{#if ipCriticos.length === 0}
|
|
|
|
{#if ipCriticos.length === 0}
|
|
|
|
<li class="text-base-content/60">Nenhum IP crítico listado.</li>
|
|
|
|
<li class="text-base-content/60">Nenhum IP crítico listado.</li>
|
|
|
|
{:else}
|
|
|
|
{:else}
|
|
|
|
@@ -1361,9 +1252,11 @@
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Bloco separado: Regras de Portas -->
|
|
|
|
<!-- Bloco separado: Regras de Portas -->
|
|
|
|
<div class="border border-base-300 rounded-2xl p-4">
|
|
|
|
<div class="border-base-300 rounded-2xl border p-4">
|
|
|
|
<h4 class="text-info text-lg font-bold">Regras de Portas Monitoradas</h4>
|
|
|
|
<h4 class="text-info text-lg font-bold">Regras de Portas Monitoradas</h4>
|
|
|
|
<div class="mt-4 space-y-2 text-xs {regras.length >= 6 ? 'max-h-64 overflow-auto pr-2' : ''}">
|
|
|
|
<div
|
|
|
|
|
|
|
|
class="mt-4 space-y-2 text-xs {regras.length >= 6 ? 'max-h-64 overflow-auto pr-2' : ''}"
|
|
|
|
|
|
|
|
>
|
|
|
|
{#if regras.length === 0}
|
|
|
|
{#if regras.length === 0}
|
|
|
|
<p class="text-base-content/60">Nenhuma regra cadastrada.</p>
|
|
|
|
<p class="text-base-content/60">Nenhuma regra cadastrada.</p>
|
|
|
|
{:else}
|
|
|
|
{:else}
|
|
|
|
@@ -1432,7 +1325,7 @@
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Bloco separado: Relatórios -->
|
|
|
|
<!-- Bloco separado: Relatórios -->
|
|
|
|
<div class="border border-base-300 rounded-2xl p-4">
|
|
|
|
<div class="border-base-300 rounded-2xl border p-4">
|
|
|
|
<h4 class="text-primary text-lg font-bold">Relatórios refinados</h4>
|
|
|
|
<h4 class="text-primary text-lg font-bold">Relatórios refinados</h4>
|
|
|
|
<form class="mt-3 space-y-2" onsubmit={gerarRelatorioAvancado}>
|
|
|
|
<form class="mt-3 space-y-2" onsubmit={gerarRelatorioAvancado}>
|
|
|
|
<label class="form-control">
|
|
|
|
<label class="form-control">
|
|
|
|
@@ -1470,7 +1363,7 @@
|
|
|
|
<h5 class="text-base-content text-sm font-semibold">Relatórios recentes</h5>
|
|
|
|
<h5 class="text-base-content text-sm font-semibold">Relatórios recentes</h5>
|
|
|
|
{#if relatoriosRecentes?.data?.length}
|
|
|
|
{#if relatoriosRecentes?.data?.length}
|
|
|
|
<div class="mt-2 overflow-x-auto">
|
|
|
|
<div class="mt-2 overflow-x-auto">
|
|
|
|
<table class="table table-sm">
|
|
|
|
<table class="table-sm table">
|
|
|
|
<thead>
|
|
|
|
<thead>
|
|
|
|
<tr>
|
|
|
|
<tr>
|
|
|
|
<th>Status</th>
|
|
|
|
<th>Status</th>
|
|
|
|
@@ -1487,7 +1380,11 @@
|
|
|
|
<span class="badge badge-outline">{r.status}</span>
|
|
|
|
<span class="badge badge-outline">{r.status}</span>
|
|
|
|
</td>
|
|
|
|
</td>
|
|
|
|
<td>{new Date(r.criadoEm).toLocaleString('pt-BR', { hour12: false })}</td>
|
|
|
|
<td>{new Date(r.criadoEm).toLocaleString('pt-BR', { hour12: false })}</td>
|
|
|
|
<td>{r.concluidoEm ? new Date(r.concluidoEm).toLocaleString('pt-BR', { hour12: false }) : '-'}</td>
|
|
|
|
<td
|
|
|
|
|
|
|
|
>{r.concluidoEm
|
|
|
|
|
|
|
|
? new Date(r.concluidoEm).toLocaleString('pt-BR', { hour12: false })
|
|
|
|
|
|
|
|
: '-'}</td
|
|
|
|
|
|
|
|
>
|
|
|
|
<td class="max-w-xs truncate">{r.observacoes ?? '-'}</td>
|
|
|
|
<td class="max-w-xs truncate">{r.observacoes ?? '-'}</td>
|
|
|
|
<td>
|
|
|
|
<td>
|
|
|
|
<div class="flex gap-2">
|
|
|
|
<div class="flex gap-2">
|
|
|
|
@@ -1790,6 +1687,215 @@
|
|
|
|
{/if}
|
|
|
|
{/if}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Alertas e Notificações -->
|
|
|
|
|
|
|
|
<section class="border-info/20 bg-base-100/80 mt-6 rounded-3xl border p-6 shadow-2xl">
|
|
|
|
|
|
|
|
<h3 class="text-info text-2xl font-bold">Alertas e Notificações</h3>
|
|
|
|
|
|
|
|
<p class="text-base-content/70 mt-1 text-sm">
|
|
|
|
|
|
|
|
Configure destinatários, níveis e tipos de alarme e reenvio.
|
|
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
<div class="mt-4 grid gap-6 md:grid-cols-2">
|
|
|
|
|
|
|
|
<!-- Emails -->
|
|
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
|
|
|
<h4 class="text-base-content font-semibold">Emails de destino</h4>
|
|
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
|
|
class="textarea textarea-bordered w-full"
|
|
|
|
|
|
|
|
rows="4"
|
|
|
|
|
|
|
|
bind:value={alertEmails}
|
|
|
|
|
|
|
|
placeholder="Um email por linha"
|
|
|
|
|
|
|
|
></textarea>
|
|
|
|
|
|
|
|
{#if sugestoesEmails.length}
|
|
|
|
|
|
|
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
|
|
|
|
|
|
{#each sugestoesEmails as s (s)}
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
|
|
class="badge badge-outline"
|
|
|
|
|
|
|
|
onclick={() => adicionarEmailSugestao(s)}>{s}</button
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
<div class="grid gap-3 md:grid-cols-2">
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Severidade mínima</span>
|
|
|
|
|
|
|
|
<select class="select select-bordered select-sm" bind:value={alertSeveridadeMin}>
|
|
|
|
|
|
|
|
{#each severidadesDisponiveis as s (s)}
|
|
|
|
|
|
|
|
<option value={s}>{severityLabels[s]}</option>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Tipos de ataque</span>
|
|
|
|
|
|
|
|
<select
|
|
|
|
|
|
|
|
class="select select-bordered select-sm"
|
|
|
|
|
|
|
|
multiple
|
|
|
|
|
|
|
|
size="5"
|
|
|
|
|
|
|
|
bind:value={alertTiposAtaque}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{#each Object.keys(attackLabels) as t (t)}
|
|
|
|
|
|
|
|
<option value={t}>{attackLabels[t as AtaqueCiberneticoTipo]}</option>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="flex items-center gap-4">
|
|
|
|
|
|
|
|
<label class="flex items-center gap-2 text-xs">
|
|
|
|
|
|
|
|
<input type="checkbox" class="checkbox checkbox-xs" bind:checked={enviarPorEmail} />
|
|
|
|
|
|
|
|
<span>Enviar por Email</span>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label class="flex items-center gap-2 text-xs">
|
|
|
|
|
|
|
|
<input type="checkbox" class="checkbox checkbox-xs" bind:checked={enviarPorChat} />
|
|
|
|
|
|
|
|
<span>Enviar por Chat</span>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Tempo de reenvio (minutos)</span>
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
|
|
max="1440"
|
|
|
|
|
|
|
|
class="input input-bordered input-sm"
|
|
|
|
|
|
|
|
bind:value={alertReenvioMin}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Template de email</span>
|
|
|
|
|
|
|
|
<select class="select select-bordered select-sm" bind:value={alertTemplate}>
|
|
|
|
|
|
|
|
<option value="incidente_critico">Incidente Crítico - Ação Imediata</option>
|
|
|
|
|
|
|
|
<option value="bloqueio_automatico">Bloqueio Automático</option>
|
|
|
|
|
|
|
|
<option value="sumario_30min">Sumário 30 Min</option>
|
|
|
|
|
|
|
|
<option value="anormalidade">Anomalia Detectada</option>
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
|
|
class="btn btn-info btn-sm"
|
|
|
|
|
|
|
|
onclick={() => salvarPreferenciasAlertas(editarAlertConfigId ?? undefined)}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{editarAlertConfigId ? 'Atualizar preferências' : 'Salvar preferências'}
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Chat -->
|
|
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
|
|
|
|
|
<h4 class="text-base-content font-semibold">Alertas por Chat</h4>
|
|
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
|
|
class="textarea textarea-bordered w-full"
|
|
|
|
|
|
|
|
rows="3"
|
|
|
|
|
|
|
|
bind:value={alertUsersChat}
|
|
|
|
|
|
|
|
placeholder="IDs de usuários de chat (um por linha)"
|
|
|
|
|
|
|
|
></textarea>
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Selecionar usuários de chat</span>
|
|
|
|
|
|
|
|
<select
|
|
|
|
|
|
|
|
class="select select-bordered select-sm"
|
|
|
|
|
|
|
|
multiple
|
|
|
|
|
|
|
|
size="6"
|
|
|
|
|
|
|
|
onchange={(e) => {
|
|
|
|
|
|
|
|
const options = Array.from((e.target as HTMLSelectElement).selectedOptions).map(
|
|
|
|
|
|
|
|
(o) => o.value
|
|
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
for (const val of options) adicionarChatSugestao(val);
|
|
|
|
|
|
|
|
}}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{#each usuariosParaChat?.data ?? [] as u (u._id)}
|
|
|
|
|
|
|
|
<option value={u.username ?? u.email}>{u.nome} ({u.username ?? u.email})</option>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
{#if sugestoesChatUsers.length}
|
|
|
|
|
|
|
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
|
|
|
|
|
|
{#each sugestoesChatUsers as s (s)}
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
|
|
class="badge badge-outline"
|
|
|
|
|
|
|
|
onclick={() => adicionarChatSugestao(s)}>{s}</button
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
<div class="grid gap-3 md:grid-cols-2">
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Severidade mínima</span>
|
|
|
|
|
|
|
|
<select class="select select-bordered select-sm" bind:value={chatSeveridadeMin}>
|
|
|
|
|
|
|
|
{#each severidadesDisponiveis as s (s)}
|
|
|
|
|
|
|
|
<option value={s}>{severityLabels[s]}</option>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Tipos de ataque</span>
|
|
|
|
|
|
|
|
<select
|
|
|
|
|
|
|
|
class="select select-bordered select-sm"
|
|
|
|
|
|
|
|
multiple
|
|
|
|
|
|
|
|
size="5"
|
|
|
|
|
|
|
|
bind:value={chatTiposAtaque}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
{#each Object.keys(attackLabels) as t (t)}
|
|
|
|
|
|
|
|
<option value={t}>{attackLabels[t as AtaqueCiberneticoTipo]}</option>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</select>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<label class="form-control">
|
|
|
|
|
|
|
|
<span class="label-text text-xs">Tempo de reenvio (minutos)</span>
|
|
|
|
|
|
|
|
<input
|
|
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
|
|
min="1"
|
|
|
|
|
|
|
|
max="1440"
|
|
|
|
|
|
|
|
class="input input-bordered input-sm"
|
|
|
|
|
|
|
|
bind:value={chatReenvioMin}
|
|
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
|
|
class="btn btn-outline btn-sm"
|
|
|
|
|
|
|
|
onclick={() => salvarPreferenciasAlertas(editarAlertConfigId ?? undefined)}
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
Salvar preferências de chat
|
|
|
|
|
|
|
|
</button>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="divider my-6"></div>
|
|
|
|
|
|
|
|
<h4 class="text-base-content text-sm font-semibold">Configurações salvas</h4>
|
|
|
|
|
|
|
|
{#if alertConfigs?.data?.length}
|
|
|
|
|
|
|
|
<div class="mt-2 grid gap-3 md:grid-cols-2">
|
|
|
|
|
|
|
|
{#each alertConfigs.data as cfg (cfg._id)}
|
|
|
|
|
|
|
|
<div class="border-base-200 rounded-xl border p-3">
|
|
|
|
|
|
|
|
<div class="flex items-center justify-between">
|
|
|
|
|
|
|
|
<div class="space-y-1">
|
|
|
|
|
|
|
|
<p class="font-semibold">{cfg.nome}</p>
|
|
|
|
|
|
|
|
<p class="text-base-content/60 text-xs">
|
|
|
|
|
|
|
|
Canais: {cfg.canais.email ? 'Email' : ''}{cfg.canais.email && cfg.canais.chat
|
|
|
|
|
|
|
|
? ' + '
|
|
|
|
|
|
|
|
: ''}{cfg.canais.chat ? 'Chat' : ''}
|
|
|
|
|
|
|
|
• Sev. mínima: {severityLabels[cfg.severidadeMin]}
|
|
|
|
|
|
|
|
• Reenvio: {cfg.reenvioMin} min
|
|
|
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
{#if cfg.emails.length}
|
|
|
|
|
|
|
|
<p class="text-base-content/60 text-xs">Emails: {cfg.emails.join(', ')}</p>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
{#if cfg.chatUsers.length}
|
|
|
|
|
|
|
|
<p class="text-base-content/60 text-xs">Chat: {cfg.chatUsers.join(', ')}</p>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<div class="flex gap-2">
|
|
|
|
|
|
|
|
<button class="btn btn-xs" type="button" onclick={() => carregarParaEdicao(cfg)}
|
|
|
|
|
|
|
|
>Editar</button
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
<button
|
|
|
|
|
|
|
|
class="btn btn-xs btn-error"
|
|
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
|
|
onclick={() => excluirPreferenciasAlertas(cfg._id)}>Excluir</button
|
|
|
|
|
|
|
|
>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{/each}
|
|
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
{:else}
|
|
|
|
|
|
|
|
<p class="text-base-content/60 mt-2 text-xs">Nenhuma configuração salva.</p>
|
|
|
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
</section>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<style>
|
|
|
|
<style>
|
|
|
|
|