267 lines
8.1 KiB
Vue
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>
|