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

267 lines
8.1 KiB
Vue

<template>
<div class="space-y-4">
<!-- Filters -->
<UCard :ui="cardUi">
<div class="flex flex-wrap items-end gap-3">
<div class="flex-1 min-w-36">
<label class="text-xs text-gray-400 mb-1 block">Тип атаки</label>
<USelect
v-model="filters.type"
:options="typeOptions"
size="sm"
placeholder="Все типы"
:ui="selectUi"
/>
</div>
<div class="flex-1 min-w-36">
<label class="text-xs text-gray-400 mb-1 block">Критичность</label>
<USelect
v-model="filters.severity"
:options="severityOptions"
size="sm"
placeholder="Все уровни"
:ui="selectUi"
/>
</div>
<div class="flex-1 min-w-36">
<label class="text-xs text-gray-400 mb-1 block">IP источника</label>
<UInput
v-model="filters.src_ip"
size="sm"
placeholder="192.168.0.1"
:ui="inputUi"
/>
</div>
<div class="flex gap-2">
<UButton size="sm" color="blue" icon="i-heroicons-magnifying-glass" @click="loadAlerts">
Найти
</UButton>
<UButton size="sm" color="gray" variant="soft" icon="i-heroicons-x-mark" @click="resetFilters">
Сбросить
</UButton>
<UButton
size="sm"
color="red"
variant="soft"
icon="i-heroicons-trash"
:loading="clearing"
@click="clearAll"
>
Очистить всё
</UButton>
</div>
</div>
</UCard>
<!-- Table -->
<UCard :ui="cardUi">
<template #header>
<div class="flex items-center justify-between">
<p class="text-sm font-semibold text-gray-200">
Инциденты
<span class="text-gray-500 font-normal ml-1">({{ total }})</span>
</p>
<div class="flex gap-2">
<UBadge
v-for="s in activeSeverities"
:key="s.severity"
:color="severityColor(s.severity)"
variant="soft"
size="xs"
>
{{ s.label }}: {{ s.count }}
</UBadge>
</div>
</div>
</template>
<UTable
:rows="alerts"
:columns="columns"
:loading="loading"
:ui="tableUi"
>
<template #severity-data="{ row }">
<SeverityBadge :severity="row.severity" />
</template>
<template #attack_type-data="{ row }">
<span class="text-xs font-mono text-blue-300">{{ typeLabel(row.attack_type) }}</span>
</template>
<template #src_ip-data="{ row }">
<span class="font-mono text-xs text-gray-300">{{ row.src_ip }}</span>
</template>
<template #dst_ip-data="{ row }">
<span class="font-mono text-xs text-gray-400">
{{ row.dst_ip }}{{ row.dst_port ? ':' + row.dst_port : '' }}
</span>
</template>
<template #created_at-data="{ row }">
<span class="text-xs text-gray-400">{{ formatTime(row.created_at) }}</span>
</template>
<template #description-data="{ row }">
<span class="text-xs text-gray-300">{{ row.description }}</span>
</template>
<template #actions-data="{ row }">
<UButton
icon="i-heroicons-trash"
size="xs"
color="red"
variant="ghost"
@click="deleteAlert(row.id)"
/>
</template>
</UTable>
<!-- Pagination -->
<div class="flex justify-between items-center mt-4 pt-4 border-t border-gray-800">
<p class="text-xs text-gray-500">
Показано {{ alerts.length }} из {{ total }}
</p>
<UPagination
v-model="page"
:page-count="pageSize"
:total="total"
size="xs"
:ui="{ wrapper: 'gap-1' }"
@update:model-value="loadAlerts"
/>
</div>
</UCard>
</div>
</template>
<script setup lang="ts">
const { get, del } = useApi()
const alerts = ref<any[]>([])
const total = ref(0)
const loading = ref(false)
const clearing = ref(false)
const page = ref(1)
const pageSize = 25
const filters = reactive({
type: '',
severity: '',
src_ip: '',
})
const columns = [
{ key: 'created_at', label: 'Время', sortable: false },
{ key: 'severity', label: 'Критичность', sortable: false },
{ key: 'attack_type', label: 'Тип', sortable: false },
{ key: 'src_ip', label: 'Источник', sortable: false },
{ key: 'dst_ip', label: 'Назначение', sortable: false },
{ key: 'description', label: 'Описание', sortable: false },
{ key: 'actions', label: '', sortable: false },
]
const typeOptions = [
{ label: 'Все типы', value: '' },
{ label: 'Port Scan', value: 'port_scan' },
{ label: 'Brute Force', value: 'brute_force' },
{ label: 'DoS/DDoS', value: 'dos' },
{ label: 'ARP Spoof', value: 'arp_spoof' },
{ label: 'DNS Anomaly', value: 'dns_anomaly' },
]
const severityOptions = [
{ label: 'Все уровни', value: '' },
{ label: 'Критический', value: 'critical' },
{ label: 'Высокий', value: 'high' },
{ label: 'Средний', value: 'medium' },
{ label: 'Низкий', value: 'low' },
]
async function loadAlerts() {
loading.value = true
try {
const params: Record<string, string | number> = {
page: page.value,
page_size: pageSize,
}
if (filters.type) params.type = filters.type
if (filters.severity) params.severity = filters.severity
if (filters.src_ip) params.src_ip = filters.src_ip
const res = await get<any>('/alerts', params)
// Копируем type → attack_type, т.к. Vue 3 перехватывает .type на реактивных объектах
alerts.value = (res.data ?? []).map((a: any) => ({ ...a, attack_type: a.type }))
total.value = res.total ?? 0
} finally {
loading.value = false
}
}
async function deleteAlert(id: number) {
await del(`/alerts/${id}`)
loadAlerts()
}
async function clearAll() {
clearing.value = true
try {
await del('/alerts')
await loadAlerts()
} finally {
clearing.value = false
}
}
function resetFilters() {
filters.type = ''
filters.severity = ''
filters.src_ip = ''
page.value = 1
loadAlerts()
}
const activeSeverities = computed(() => {
const counts: Record<string, number> = {}
alerts.value.forEach(a => { counts[a.severity] = (counts[a.severity] ?? 0) + 1 })
return Object.entries(counts).map(([severity, count]) => ({
severity,
count,
label: { critical: 'Крит', high: 'Выс', medium: 'Ср', low: 'Низ' }[severity] ?? severity,
}))
})
function severityColor(s: string): 'red' | 'orange' | 'yellow' | 'green' | 'gray' {
return ({ critical: 'red', high: 'orange', medium: 'yellow', low: 'green' } as any)[s] ?? 'gray'
}
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',
}
function typeLabel(t: string | undefined): string {
if (!t) return '—'
return TYPE_LABELS[t] ?? t
}
function formatTime(iso: string) {
return new Date(iso).toLocaleString('ru-RU', {
day: '2-digit', month: '2-digit', year: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit',
})
}
onMounted(loadAlerts)
// UI tokens
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-3' } }
const tableUi = { wrapper: 'overflow-x-auto', td: { base: 'py-2' }, th: { base: 'text-gray-400 text-xs font-medium' } }
const selectUi = { base: 'bg-gray-800 border-gray-700 text-gray-200 text-sm' }
const inputUi = { base: 'bg-gray-800 border-gray-700 text-gray-200 text-sm' }
</script>