Files
dip-ids/frontend/pages/stats.vue
T
2026-04-12 22:26:26 +04:00

234 lines
9.1 KiB
Vue

<template>
<div class="space-y-4">
<!-- Period selector -->
<div class="flex gap-2">
<UButton
v-for="h in periodOptions"
:key="h"
:color="hours === h ? 'blue' : 'gray'"
:variant="hours === h ? 'solid' : 'soft'"
size="xs"
@click="hours = h; loadAll()"
>
{{ periodLabel(h) }}
</UButton>
</div>
<!-- Timeline chart -->
<UCard :ui="cardUi">
<template #header>
<p class="text-sm font-semibold text-gray-200">Динамика алертов</p>
</template>
<div class="h-64">
<BarChart v-if="timelineReady" :data="timelineData" :options="barOptions" />
<div v-else class="h-full flex items-center justify-center text-gray-600 text-sm">Нет данных</div>
</div>
</UCard>
<!-- Second row: Types + Severity -->
<div class="grid grid-cols-2 gap-4">
<UCard :ui="cardUi">
<template #header>
<p class="text-sm font-semibold text-gray-200">По типам атак</p>
</template>
<div class="h-56 flex items-center justify-center">
<DoughnutChart v-if="typesReady" :data="typesChartData" :options="doughnutOptions" />
<div v-else class="text-gray-600 text-sm">Нет данных</div>
</div>
</UCard>
<UCard :ui="cardUi">
<template #header>
<p class="text-sm font-semibold text-gray-200">По уровню критичности</p>
</template>
<div class="h-56 flex items-center justify-center">
<DoughnutChart v-if="severityReady" :data="severityChartData" :options="doughnutOptions" />
<div v-else class="text-gray-600 text-sm">Нет данных</div>
</div>
</UCard>
</div>
<!-- Top IPs bar -->
<UCard :ui="cardUi">
<template #header>
<p class="text-sm font-semibold text-gray-200">Топ-10 источников атак</p>
</template>
<div class="h-56">
<BarChart v-if="ipsReady" :data="ipsChartData" :options="ipsBarOptions" />
<div v-else class="h-full flex items-center justify-center text-gray-600 text-sm">Нет данных</div>
</div>
</UCard>
<!-- Summary table -->
<UCard :ui="cardUi">
<template #header>
<p class="text-sm font-semibold text-gray-200">Сводная статистика</p>
</template>
<div class="grid grid-cols-3 gap-6">
<div>
<p class="text-xs text-gray-500 mb-2 font-medium uppercase tracking-wide">По критичности</p>
<div class="space-y-1.5">
<div v-for="s in severityRows" :key="s.severity" class="flex items-center justify-between">
<SeverityBadge :severity="s.severity" />
<span class="text-sm font-mono text-gray-300">{{ s.count }}</span>
</div>
</div>
</div>
<div>
<p class="text-xs text-gray-500 mb-2 font-medium uppercase tracking-wide">По типу</p>
<div class="space-y-1.5">
<div v-for="t in typeRows" :key="t.type" class="flex items-center justify-between">
<span class="text-xs text-blue-300 font-mono">{{ typeLabel(t.type) }}</span>
<span class="text-sm font-mono text-gray-300">{{ t.count }}</span>
</div>
</div>
</div>
<div>
<p class="text-xs text-gray-500 mb-2 font-medium uppercase tracking-wide">Итого</p>
<p class="text-3xl font-bold text-white">{{ summary.total ?? 0 }}</p>
<p class="text-xs text-gray-500 mt-1">алертов зафиксировано</p>
</div>
</div>
</UCard>
</div>
</template>
<script setup lang="ts">
import {
Chart as ChartJS, CategoryScale, LinearScale,
BarElement, ArcElement, Tooltip, Legend,
} from 'chart.js'
import { Bar as BarChart, Doughnut as DoughnutChart } from 'vue-chartjs'
ChartJS.register(CategoryScale, LinearScale, BarElement, ArcElement, Tooltip, Legend)
const { get } = useApi()
const hours = ref(24)
const periodOptions = [6, 24, 48, 168]
const summary = ref<any>({})
const timeline = ref<any[]>([])
const topIPs = ref<any[]>([])
async function loadAll() {
const [sum, tl, ips] = await Promise.all([
get<any>('/stats/summary'),
get<any>('/stats/timeline', { hours: hours.value }),
get<any>('/stats/top-ips', { limit: 10 }),
])
summary.value = sum
timeline.value = tl.data ?? []
topIPs.value = ips.data ?? []
}
onMounted(loadAll)
function periodLabel(h: number) {
return h < 24 ? `${h}ч` : h === 24 ? '24ч' : h === 48 ? '2д' : '7д'
}
// ── Timeline chart ────────────────────────────────────────────────────────────
const timelineReady = computed(() => timeline.value.length > 0)
const timelineData = computed(() => ({
labels: timeline.value.map(p => p.hour?.slice(11, 16) ?? p.hour),
datasets: [{
label: 'Алерты',
data: timeline.value.map(p => p.count),
backgroundColor: 'rgba(59,130,246,0.7)',
borderRadius: 4,
}],
}))
const barOptions = {
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { color: '#6b7280', font: { size: 10 } } },
y: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { color: '#6b7280', font: { size: 10 } }, beginAtZero: true },
},
}
// ── Types chart ───────────────────────────────────────────────────────────────
const TYPE_COLORS: Record<string, string> = {
port_scan: '#3b82f6', brute_force: '#ef4444',
dos: '#f97316', arp_spoof: '#a855f7', dns_anomaly: '#22c55e',
}
const TYPE_LABELS: Record<string, string> = {
port_scan: 'Port Scan', brute_force: 'Brute Force',
dos: 'DoS/DDoS', arp_spoof: 'ARP Spoof', dns_anomaly: 'DNS Anomaly',
}
const typesReady = computed(() => (summary.value.by_type ?? []).length > 0)
const typesChartData = computed(() => {
const types = summary.value.by_type ?? []
return {
labels: types.map((t: any) => TYPE_LABELS[t.type] ?? t.type),
datasets: [{ data: types.map((t: any) => t.count), backgroundColor: types.map((t: any) => TYPE_COLORS[t.type] ?? '#6b7280'), borderWidth: 0 }],
}
})
// ── Severity chart ────────────────────────────────────────────────────────────
const SEV_COLORS: Record<string, string> = {
critical: '#ef4444', high: '#f97316', medium: '#eab308', low: '#22c55e',
}
const SEV_LABELS: Record<string, string> = {
critical: 'Критический', high: 'Высокий', medium: 'Средний', low: 'Низкий',
}
const severityReady = computed(() => Object.values(summary.value.by_severity ?? {}).some(v => v))
const severityChartData = computed(() => {
const sev = summary.value.by_severity ?? {}
const order = ['critical', 'high', 'medium', 'low']
return {
labels: order.map(s => SEV_LABELS[s]),
datasets: [{ data: order.map(s => sev[s] ?? 0), backgroundColor: order.map(s => SEV_COLORS[s]), borderWidth: 0 }],
}
})
const doughnutOptions = {
responsive: true, maintainAspectRatio: false, cutout: '65%',
plugins: {
legend: { position: 'right' as const, labels: { color: '#9ca3af', font: { size: 10 }, padding: 10, boxWidth: 10 } },
},
}
// ── IPs bar chart ─────────────────────────────────────────────────────────────
const ipsReady = computed(() => topIPs.value.length > 0)
const ipsChartData = computed(() => ({
labels: topIPs.value.map(ip => ip.src_ip),
datasets: [{
label: 'Алертов',
data: topIPs.value.map(ip => ip.count),
backgroundColor: 'rgba(168,85,247,0.7)',
borderRadius: 4,
}],
}))
const ipsBarOptions = {
indexAxis: 'y' as const,
responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { grid: { color: 'rgba(255,255,255,0.04)' }, ticks: { color: '#6b7280', font: { size: 10 } }, beginAtZero: true },
y: { grid: { display: false }, ticks: { color: '#d1d5db', font: { size: 10, family: 'monospace' } } },
},
}
// ── Tables ────────────────────────────────────────────────────────────────────
const SEV_ORDER = ['critical', 'high', 'medium', 'low']
const severityRows = computed(() => {
const sev = summary.value.by_severity ?? {}
return SEV_ORDER.map(s => ({ severity: s, count: sev[s] ?? 0 }))
})
const typeRows = computed(() => summary.value.by_type ?? [])
function typeLabel(t: string | undefined): string {
if (!t) return '—'
return TYPE_LABELS[t] ?? t
}
const cardUi = { base: 'bg-gray-900 border border-gray-800', header: { base: 'border-b border-gray-800 px-4 py-3' }, body: { base: 'px-4 py-4' } }
</script>