feat: healthcheck backend and fix data types
also small various features
This commit is contained in:
@@ -1,13 +1,55 @@
|
||||
const RETRY_DELAYS = [0, 500, 1500]
|
||||
const CACHE_TTL_MS = 24 * 60 * 60 * 1000
|
||||
|
||||
function cacheKey(url: string) {
|
||||
return `nids_cache:${url}`
|
||||
}
|
||||
|
||||
function saveToCache(url: string, data: unknown) {
|
||||
try {
|
||||
localStorage.setItem(cacheKey(url), JSON.stringify({ data, ts: Date.now() }))
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function loadFromCache<T>(url: string): T | null {
|
||||
try {
|
||||
const raw = localStorage.getItem(cacheKey(url))
|
||||
if (!raw) return null
|
||||
const { data, ts } = JSON.parse(raw)
|
||||
if (Date.now() - ts > CACHE_TTL_MS) return null
|
||||
return data as T
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Базовый fetch к Go API
|
||||
export function useApi() {
|
||||
async function get<T>(path: string, params?: Record<string, string | number>) {
|
||||
async function get<T>(path: string, params?: Record<string, string | number>): Promise<T> {
|
||||
const url = new URL('/api' + path, window.location.origin)
|
||||
if (params) {
|
||||
Object.entries(params).forEach(([k, v]) => url.searchParams.set(k, String(v)))
|
||||
}
|
||||
const res = await fetch(url.toString())
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
return res.json() as Promise<T>
|
||||
const urlStr = url.toString()
|
||||
|
||||
let lastErr: unknown
|
||||
for (let i = 0; i < RETRY_DELAYS.length; i++) {
|
||||
if (i > 0) await new Promise(r => setTimeout(r, RETRY_DELAYS[i]))
|
||||
try {
|
||||
const res = await fetch(urlStr, { signal: AbortSignal.timeout(5000) })
|
||||
if (!res.ok) throw new Error(await res.text())
|
||||
const data = await res.json() as T
|
||||
saveToCache(urlStr, data)
|
||||
return data
|
||||
} catch (e) {
|
||||
lastErr = e
|
||||
}
|
||||
}
|
||||
|
||||
const cached = loadFromCache<T>(urlStr)
|
||||
if (cached !== null) return cached
|
||||
|
||||
throw lastErr
|
||||
}
|
||||
|
||||
async function post<T>(path: string, body?: unknown) {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// Singleton: один interval на всё приложение
|
||||
let checkInterval: number | null = null
|
||||
const isOnline = ref<boolean>(true)
|
||||
|
||||
export function useBackendStatus() {
|
||||
async function check() {
|
||||
try {
|
||||
const res = await fetch('/api/health', { signal: AbortSignal.timeout(3000) })
|
||||
isOnline.value = res.ok
|
||||
} catch {
|
||||
isOnline.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function startPolling() {
|
||||
if (checkInterval || !import.meta.client) return
|
||||
check()
|
||||
checkInterval = window.setInterval(check, 5000)
|
||||
}
|
||||
|
||||
onMounted(startPolling)
|
||||
|
||||
return { isOnline, check }
|
||||
}
|
||||
@@ -31,8 +31,8 @@
|
||||
</NuxtLink>
|
||||
</nav>
|
||||
|
||||
<!-- Status indicator -->
|
||||
<div class="px-4 py-4 border-t border-gray-800">
|
||||
<!-- Status indicators -->
|
||||
<div class="px-4 py-4 border-t border-gray-800 space-y-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="w-2 h-2 rounded-full shrink-0"
|
||||
@@ -42,6 +42,15 @@
|
||||
{{ captureRunning ? 'Захват активен' : 'Захват остановлен' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span
|
||||
class="w-2 h-2 rounded-full shrink-0"
|
||||
:class="isOnline ? 'bg-green-400' : 'bg-red-500 animate-pulse'"
|
||||
/>
|
||||
<span class="text-xs" :class="isOnline ? 'text-gray-400' : 'text-red-400'">
|
||||
{{ isOnline ? 'Бэкенд онлайн' : 'Бэкенд недоступен' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -71,6 +80,15 @@
|
||||
</UButton>
|
||||
</header>
|
||||
|
||||
<!-- Offline banner -->
|
||||
<div
|
||||
v-if="!isOnline"
|
||||
class="mx-6 mt-4 flex items-center gap-2 text-xs text-yellow-300 bg-yellow-950/50 border border-yellow-800/60 rounded-lg px-3 py-2 shrink-0"
|
||||
>
|
||||
<UIcon name="i-heroicons-signal-slash" class="w-4 h-4 shrink-0" />
|
||||
Бэкенд недоступен — отображаются данные из БД
|
||||
</div>
|
||||
|
||||
<!-- Page content -->
|
||||
<main class="flex-1 overflow-auto p-6">
|
||||
<slot />
|
||||
@@ -82,6 +100,7 @@
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
const { status, stopCapture } = useCaptureStatus()
|
||||
const { isOnline } = useBackendStatus()
|
||||
|
||||
const captureRunning = computed(() => status.value?.running ?? false)
|
||||
|
||||
|
||||
@@ -11,9 +11,10 @@ export default defineNuxtConfig({
|
||||
|
||||
// Проксируем только маршруты Go бэкенда (не трогаем /api/_nuxt_icon и др. внутренние)
|
||||
routeRules: {
|
||||
'/api/alerts/**': { proxy: 'http://localhost:8080/api/alerts/**' },
|
||||
'/api/alerts': { proxy: 'http://localhost:8080/api/alerts' },
|
||||
'/api/stats/**': { proxy: 'http://localhost:8080/api/stats/**' },
|
||||
'/api/health': { proxy: 'http://localhost:8080/api/health' },
|
||||
'/api/alerts/**': { proxy: 'http://localhost:8080/api/alerts/**' },
|
||||
'/api/alerts': { proxy: 'http://localhost:8080/api/alerts' },
|
||||
'/api/stats/**': { proxy: 'http://localhost:8080/api/stats/**' },
|
||||
'/api/capture/**': { proxy: 'http://localhost:8080/api/capture/**' },
|
||||
'/api/reports/**': { proxy: 'http://localhost:8080/api/reports/**' },
|
||||
},
|
||||
|
||||
@@ -154,10 +154,13 @@ async function loadAll() {
|
||||
get<any>('/stats/top-ips', { limit: 5 }),
|
||||
get<any>('/alerts', { page_size: 8 }),
|
||||
])
|
||||
summary.value = sum
|
||||
summary.value = {
|
||||
...sum,
|
||||
by_type: (sum.by_type ?? []).map((t: any) => ({ ...t, attack_type: t.type })),
|
||||
}
|
||||
timeline.value = tl.data ?? []
|
||||
topIPs.value = ips.data ?? []
|
||||
recentAlerts.value = alerts.data ?? []
|
||||
recentAlerts.value = (alerts.data ?? []).map((a: any) => ({ ...a, attack_type: a.type }))
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -216,10 +219,10 @@ const typesReady = computed(() => summary.value.by_type?.length > 0)
|
||||
const typesData = computed(() => {
|
||||
const types: any[] = summary.value.by_type ?? []
|
||||
return {
|
||||
labels: types.map(t => TYPE_LABELS[t.type] ?? t.type ?? '—'),
|
||||
labels: types.map(t => TYPE_LABELS[t.attack_type] ?? t.attack_type ?? '—'),
|
||||
datasets: [{
|
||||
data: types.map(t => t.count),
|
||||
backgroundColor: types.map(t => TYPE_COLORS[t.type] ?? '#6b7280'),
|
||||
backgroundColor: types.map(t => TYPE_COLORS[t.attack_type] ?? '#6b7280'),
|
||||
borderWidth: 0,
|
||||
}],
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"dip-ids/internal/analyzer"
|
||||
"dip-ids/internal/api/handlers"
|
||||
@@ -27,7 +28,7 @@ func New(
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(corsMiddleware())
|
||||
r.Use(gin.LoggerWithConfig(gin.LoggerConfig{
|
||||
SkipPaths: []string{"/api/capture/status"},
|
||||
SkipPaths: []string{"/api/capture/status", "/api/health"},
|
||||
}))
|
||||
|
||||
alertsH := handlers.NewAlertsHandler(db)
|
||||
@@ -37,6 +38,10 @@ func New(
|
||||
|
||||
api := r.Group("/api")
|
||||
|
||||
api.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
})
|
||||
|
||||
api.GET("/alerts", alertsH.GetAlerts)
|
||||
api.DELETE("/alerts", alertsH.ClearAlerts)
|
||||
api.DELETE("/alerts/:id", alertsH.DeleteAlert)
|
||||
|
||||
+183
-104
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/johnfercher/maroto/v2/pkg/components/text"
|
||||
"github.com/johnfercher/maroto/v2/pkg/config"
|
||||
"github.com/johnfercher/maroto/v2/pkg/consts/align"
|
||||
"github.com/johnfercher/maroto/v2/pkg/consts/border"
|
||||
"github.com/johnfercher/maroto/v2/pkg/consts/fontstyle"
|
||||
"github.com/johnfercher/maroto/v2/pkg/core"
|
||||
"github.com/johnfercher/maroto/v2/pkg/props"
|
||||
@@ -33,7 +34,6 @@ type Data struct {
|
||||
RecentStats []storage.PacketStat
|
||||
}
|
||||
|
||||
// Пути к шрифту DejaVu Sans с поддержкой кириллицы
|
||||
var cyrillicFontCandidates = []string{
|
||||
"/usr/share/fonts/TTF/DejaVuSans.ttf",
|
||||
"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
@@ -59,6 +59,17 @@ func findFont(candidates []string) string {
|
||||
|
||||
const fontFamily = "DejaVu"
|
||||
|
||||
// цветовая палитра
|
||||
var (
|
||||
colorBlue = &props.Color{Red: 37, Green: 99, Blue: 235}
|
||||
colorBlueDark = &props.Color{Red: 30, Green: 64, Blue: 175}
|
||||
colorWhite = &props.Color{Red: 255, Green: 255, Blue: 255}
|
||||
colorGray100 = &props.Color{Red: 248, Green: 250, Blue: 252}
|
||||
colorGray200 = &props.Color{Red: 226, Green: 232, Blue: 240}
|
||||
colorGray500 = &props.Color{Red: 100, Green: 116, Blue: 139}
|
||||
colorGray700 = &props.Color{Red: 55, Green: 65, Blue: 81}
|
||||
)
|
||||
|
||||
// Generate создаёт PDF-отчёт и возвращает байты
|
||||
func Generate(d Data) ([]byte, error) {
|
||||
normalFont := findFont(cyrillicFontCandidates)
|
||||
@@ -71,14 +82,19 @@ func Generate(d Data) ([]byte, error) {
|
||||
WithBottomMargin(15)
|
||||
|
||||
if normalFont != "" && boldFont != "" {
|
||||
builder = builder.
|
||||
WithCustomFonts([]*entity.CustomFont{
|
||||
{Family: fontFamily, Style: fontstyle.Normal, File: normalFont},
|
||||
{Family: fontFamily, Style: fontstyle.Bold, File: boldFont},
|
||||
{Family: fontFamily, Style: fontstyle.Italic, File: normalFont},
|
||||
{Family: fontFamily, Style: fontstyle.BoldItalic, File: boldFont},
|
||||
}).
|
||||
WithDefaultFont(&props.Font{Family: fontFamily, Size: 9})
|
||||
fonts, err := repository.New().
|
||||
AddUTF8Font(fontFamily, fontstyle.Normal, normalFont).
|
||||
AddUTF8Font(fontFamily, fontstyle.Bold, boldFont).
|
||||
AddUTF8Font(fontFamily, fontstyle.Italic, normalFont).
|
||||
AddUTF8Font(fontFamily, fontstyle.BoldItalic, boldFont).
|
||||
Load()
|
||||
if err != nil {
|
||||
log.Printf("[report] failed to load DejaVu font: %v", err)
|
||||
} else {
|
||||
builder = builder.
|
||||
WithCustomFonts(fonts).
|
||||
WithDefaultFont(&props.Font{Family: fontFamily, Size: 9})
|
||||
}
|
||||
} else {
|
||||
log.Println("[report] DejaVu font not found, Cyrillic may not render. Install ttf-dejavu.")
|
||||
}
|
||||
@@ -87,45 +103,54 @@ func Generate(d Data) ([]byte, error) {
|
||||
|
||||
// ── Заголовок ─────────────────────────────────────────────────────────────
|
||||
m.AddRows(buildHeader(d)...)
|
||||
m.AddRow(3, line.NewCol(12, props.Line{Color: &props.Color{Red: 37, Green: 99, Blue: 235}}))
|
||||
m.AddRow(2, line.NewCol(12, props.Line{Color: colorBlue, Thickness: 0.5}))
|
||||
m.AddRows(buildMeta(d)...)
|
||||
m.AddRow(5)
|
||||
m.AddRow(6)
|
||||
|
||||
// ── Сводка по критичности ─────────────────────────────────────────────────
|
||||
m.AddRows(buildSectionTitle("1. Сводка по уровням критичности")...)
|
||||
m.AddRow(2)
|
||||
m.AddRows(buildSeverityTable(d.BySeverity)...)
|
||||
m.AddRow(4)
|
||||
m.AddRow(6)
|
||||
|
||||
// ── Типы угроз ────────────────────────────────────────────────────────────
|
||||
m.AddRows(buildSectionTitle("2. Типы обнаруженных угроз")...)
|
||||
m.AddRow(2)
|
||||
m.AddRows(buildTypeTable(d.ByType)...)
|
||||
m.AddRow(4)
|
||||
m.AddRow(6)
|
||||
|
||||
// ── Топ источников ────────────────────────────────────────────────────────
|
||||
if len(d.TopIPs) > 0 {
|
||||
m.AddRows(buildSectionTitle("3. Топ источников атак")...)
|
||||
m.AddRow(2)
|
||||
m.AddRows(buildTopIPsTable(d.TopIPs)...)
|
||||
m.AddRow(4)
|
||||
m.AddRow(6)
|
||||
}
|
||||
|
||||
// ── Статистика трафика по протоколам ──────────────────────────────────────
|
||||
if len(d.TrafficByProto) > 0 {
|
||||
m.AddRows(buildSectionTitle("4. Статистика трафика по протоколам")...)
|
||||
m.AddRow(2)
|
||||
m.AddRows(buildTrafficProtoTable(d.TrafficByProto)...)
|
||||
m.AddRow(4)
|
||||
m.AddRow(6)
|
||||
}
|
||||
|
||||
// ── Лог трафика (последние записи) ────────────────────────────────────────
|
||||
if len(d.RecentStats) > 0 {
|
||||
m.AddRows(buildSectionTitle(fmt.Sprintf("5. Журнал трафика (последние %d записей)", len(d.RecentStats)))...)
|
||||
limit := len(d.RecentStats)
|
||||
if limit > 100 {
|
||||
limit = 100
|
||||
}
|
||||
m.AddRows(buildSectionTitle(fmt.Sprintf("5. Журнал трафика (последние %d записей)", limit))...)
|
||||
m.AddRow(2)
|
||||
m.AddRows(buildTrafficLogHeader()...)
|
||||
for i, s := range d.RecentStats {
|
||||
if i >= 100 {
|
||||
break
|
||||
}
|
||||
m.AddRows(buildTrafficLogRow(s)...)
|
||||
m.AddRows(buildTrafficLogRow(s, i)...)
|
||||
}
|
||||
m.AddRow(4)
|
||||
m.AddRow(6)
|
||||
}
|
||||
|
||||
// ── Журнал инцидентов ─────────────────────────────────────────────────────
|
||||
@@ -134,12 +159,13 @@ func Generate(d Data) ([]byte, error) {
|
||||
sectionNum = 4
|
||||
}
|
||||
m.AddRows(buildSectionTitle(fmt.Sprintf("%d. Журнал инцидентов", sectionNum))...)
|
||||
m.AddRow(2)
|
||||
m.AddRows(buildAlertsTableHeader()...)
|
||||
for i, a := range d.Alerts {
|
||||
if i >= 200 {
|
||||
break
|
||||
}
|
||||
m.AddRows(buildAlertRow(a)...)
|
||||
m.AddRows(buildAlertRow(a, i)...)
|
||||
}
|
||||
|
||||
doc, err := m.Generate()
|
||||
@@ -149,21 +175,22 @@ func Generate(d Data) ([]byte, error) {
|
||||
return doc.GetBytes(), nil
|
||||
}
|
||||
|
||||
// ── Секции отчёта ─────────────────────────────────────────────────────────────
|
||||
// ── Заголовок отчёта ──────────────────────────────────────────────────────────
|
||||
|
||||
func buildHeader(d Data) []core.Row {
|
||||
return []core.Row{
|
||||
row.New(14).Add(
|
||||
row.New(10).Add(
|
||||
col.New(8).Add(
|
||||
text.New("СИСТЕМА ОБНАРУЖЕНИЯ ВТОРЖЕНИЙ", props.Text{
|
||||
Size: 16,
|
||||
Size: 14,
|
||||
Style: fontstyle.Bold,
|
||||
Color: &props.Color{Red: 37, Green: 99, Blue: 235},
|
||||
Color: colorBlue,
|
||||
Top: 1,
|
||||
}),
|
||||
text.New("Отчёт об инцидентах сетевой безопасности", props.Text{
|
||||
Size: 9,
|
||||
Top: 8,
|
||||
Color: &props.Color{Red: 100, Green: 100, Blue: 100},
|
||||
Top: 7,
|
||||
Color: colorGray500,
|
||||
}),
|
||||
),
|
||||
col.New(4).Add(
|
||||
@@ -171,13 +198,14 @@ func buildHeader(d Data) []core.Row {
|
||||
Size: 9,
|
||||
Align: align.Right,
|
||||
Style: fontstyle.Bold,
|
||||
Color: &props.Color{Red: 37, Green: 99, Blue: 235},
|
||||
Color: colorBlue,
|
||||
Top: 1,
|
||||
}),
|
||||
text.New(fmt.Sprintf("Дата: %s", d.GeneratedAt.Format("02.01.2006 15:04:05")), props.Text{
|
||||
Size: 8,
|
||||
Top: 5,
|
||||
Top: 6,
|
||||
Align: align.Right,
|
||||
Color: &props.Color{Red: 120, Green: 120, Blue: 120},
|
||||
Color: colorGray500,
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -197,38 +225,52 @@ func buildMeta(d Data) []core.Row {
|
||||
duration = "активна"
|
||||
}
|
||||
}
|
||||
return []core.Row{
|
||||
row.New(5).Add(
|
||||
col.New(3).Add(metaCell("Источник", source)),
|
||||
col.New(3).Add(metaCell("Режим", mode)),
|
||||
col.New(3).Add(metaCell("Длительность", duration)),
|
||||
col.New(3).Add(metaCell("Пакетов / Объём", pkts+" / "+bytes)),
|
||||
),
|
||||
}
|
||||
r := row.New(8).Add(
|
||||
col.New(3).Add(metaCell("Источник", source)),
|
||||
col.New(3).Add(metaCell("Режим", mode)),
|
||||
col.New(3).Add(metaCell("Длительность", duration)),
|
||||
col.New(3).Add(metaCell("Пакетов / Объём", pkts+" / "+bytes)),
|
||||
)
|
||||
r.WithStyle(&props.Cell{
|
||||
BackgroundColor: colorGray100,
|
||||
BorderColor: colorGray200,
|
||||
BorderType: border.Full,
|
||||
BorderThickness: 0.3,
|
||||
})
|
||||
return []core.Row{r}
|
||||
}
|
||||
|
||||
func metaCell(label, value string) core.Component {
|
||||
return text.New(label+": "+value, props.Text{
|
||||
Size: 8,
|
||||
Color: &props.Color{Red: 80, Green: 80, Blue: 80},
|
||||
Left: 2,
|
||||
Top: 2,
|
||||
Color: colorGray700,
|
||||
})
|
||||
}
|
||||
|
||||
func buildSectionTitle(title string) []core.Row {
|
||||
return []core.Row{
|
||||
row.New(8).Add(
|
||||
col.New(12).Add(
|
||||
text.New(title, props.Text{
|
||||
Size: 11,
|
||||
Style: fontstyle.Bold,
|
||||
Color: &props.Color{Red: 37, Green: 99, Blue: 235},
|
||||
}),
|
||||
),
|
||||
r := row.New(8).Add(
|
||||
col.New(12).Add(
|
||||
text.New(title, props.Text{
|
||||
Size: 10,
|
||||
Style: fontstyle.Bold,
|
||||
Color: colorBlueDark,
|
||||
Left: 1,
|
||||
Top: 2,
|
||||
}),
|
||||
),
|
||||
}
|
||||
)
|
||||
r.WithStyle(&props.Cell{
|
||||
BackgroundColor: &props.Color{Red: 239, Green: 246, Blue: 255},
|
||||
BorderColor: colorBlue,
|
||||
BorderType: border.Left,
|
||||
BorderThickness: 2,
|
||||
})
|
||||
return []core.Row{r}
|
||||
}
|
||||
|
||||
// ── Таблицы сводки ────────────────────────────────────────────────────────────
|
||||
// ── Таблицы ───────────────────────────────────────────────────────────────────
|
||||
|
||||
func buildSeverityTable(data []storage.SeverityCount) []core.Row {
|
||||
rows := []core.Row{tableHeaderRow([]colDef{
|
||||
@@ -239,13 +281,23 @@ func buildSeverityTable(data []storage.SeverityCount) []core.Row {
|
||||
for _, s := range data {
|
||||
counts[s.Severity] = s.Count
|
||||
}
|
||||
for _, sev := range []string{"critical", "high", "medium", "low"} {
|
||||
rows = append(rows, row.New(6).Add(
|
||||
for i, sev := range []string{"critical", "high", "medium", "low"} {
|
||||
r := row.New(7).Add(
|
||||
col.New(6).Add(text.New(severityLabel(sev), props.Text{
|
||||
Size: 9, Style: fontstyle.Bold, Color: severityColor(sev),
|
||||
Size: 9,
|
||||
Style: fontstyle.Bold,
|
||||
Color: severityColor(sev),
|
||||
Left: 2,
|
||||
Top: 2,
|
||||
})),
|
||||
col.New(6).Add(text.New(fmt.Sprintf("%d", counts[sev]), props.Text{Size: 9})),
|
||||
))
|
||||
col.New(6).Add(text.New(fmt.Sprintf("%d", counts[sev]), props.Text{
|
||||
Size: 9,
|
||||
Left: 2,
|
||||
Top: 2,
|
||||
})),
|
||||
)
|
||||
r.WithStyle(dataRowStyle(i))
|
||||
rows = append(rows, r)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
@@ -255,11 +307,13 @@ func buildTypeTable(data []storage.TypeCount) []core.Row {
|
||||
{w: 6, label: "Тип атаки"},
|
||||
{w: 6, label: "Количество"},
|
||||
})}
|
||||
for _, t := range data {
|
||||
rows = append(rows, row.New(6).Add(
|
||||
col.New(6).Add(text.New(typeLabel(t.Type), props.Text{Size: 9})),
|
||||
col.New(6).Add(text.New(fmt.Sprintf("%d", t.Count), props.Text{Size: 9})),
|
||||
))
|
||||
for i, t := range data {
|
||||
r := row.New(7).Add(
|
||||
col.New(6).Add(text.New(typeLabel(t.Type), props.Text{Size: 9, Left: 2, Top: 2})),
|
||||
col.New(6).Add(text.New(fmt.Sprintf("%d", t.Count), props.Text{Size: 9, Left: 2, Top: 2})),
|
||||
)
|
||||
r.WithStyle(dataRowStyle(i))
|
||||
rows = append(rows, r)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
@@ -267,39 +321,39 @@ func buildTypeTable(data []storage.TypeCount) []core.Row {
|
||||
func buildTopIPsTable(data []storage.IPCount) []core.Row {
|
||||
rows := []core.Row{tableHeaderRow([]colDef{
|
||||
{w: 1, label: "#"},
|
||||
{w: 7, label: "IP-адрес источника"},
|
||||
{w: 4, label: "Алертов"},
|
||||
{w: 8, label: "IP-адрес источника"},
|
||||
{w: 3, label: "Алертов"},
|
||||
})}
|
||||
for i, ip := range data {
|
||||
rows = append(rows, row.New(6).Add(
|
||||
col.New(1).Add(text.New(fmt.Sprintf("%d", i+1), props.Text{Size: 8})),
|
||||
col.New(7).Add(text.New(ip.SrcIP, props.Text{Size: 8})),
|
||||
col.New(4).Add(text.New(fmt.Sprintf("%d", ip.Count), props.Text{Size: 8})),
|
||||
))
|
||||
r := row.New(7).Add(
|
||||
col.New(1).Add(text.New(fmt.Sprintf("%d", i+1), props.Text{Size: 8, Left: 2, Top: 2})),
|
||||
col.New(8).Add(text.New(ip.SrcIP, props.Text{Size: 8, Left: 2, Top: 2})),
|
||||
col.New(3).Add(text.New(fmt.Sprintf("%d", ip.Count), props.Text{Size: 8, Left: 2, Top: 2})),
|
||||
)
|
||||
r.WithStyle(dataRowStyle(i))
|
||||
rows = append(rows, r)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// ── Таблица трафика по протоколам ─────────────────────────────────────────────
|
||||
|
||||
func buildTrafficProtoTable(data []storage.ProtocolCount) []core.Row {
|
||||
rows := []core.Row{tableHeaderRow([]colDef{
|
||||
{w: 4, label: "Протокол"},
|
||||
{w: 4, label: "Пакетов"},
|
||||
{w: 4, label: "Объём трафика"},
|
||||
})}
|
||||
for _, p := range data {
|
||||
rows = append(rows, row.New(6).Add(
|
||||
col.New(4).Add(text.New(p.Protocol, props.Text{Size: 9, Style: fontstyle.Bold})),
|
||||
col.New(4).Add(text.New(fmt.Sprintf("%d", p.Count), props.Text{Size: 9})),
|
||||
col.New(4).Add(text.New(humanBytes(p.Bytes), props.Text{Size: 9})),
|
||||
))
|
||||
for i, p := range data {
|
||||
r := row.New(7).Add(
|
||||
col.New(4).Add(text.New(p.Protocol, props.Text{Size: 9, Style: fontstyle.Bold, Left: 2, Top: 2})),
|
||||
col.New(4).Add(text.New(fmt.Sprintf("%d", p.Count), props.Text{Size: 9, Left: 2, Top: 2})),
|
||||
col.New(4).Add(text.New(humanBytes(p.Bytes), props.Text{Size: 9, Left: 2, Top: 2})),
|
||||
)
|
||||
r.WithStyle(dataRowStyle(i))
|
||||
rows = append(rows, r)
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// ── Журнал трафика ────────────────────────────────────────────────────────────
|
||||
|
||||
func buildTrafficLogHeader() []core.Row {
|
||||
return []core.Row{tableHeaderRow([]colDef{
|
||||
{w: 3, label: "Время"},
|
||||
@@ -309,41 +363,43 @@ func buildTrafficLogHeader() []core.Row {
|
||||
})}
|
||||
}
|
||||
|
||||
func buildTrafficLogRow(s storage.PacketStat) []core.Row {
|
||||
return []core.Row{
|
||||
row.New(5).Add(
|
||||
col.New(3).Add(text.New(s.CreatedAt.Format("02.01 15:04:05"), props.Text{Size: 7})),
|
||||
col.New(2).Add(text.New(s.Protocol, props.Text{Size: 7, Style: fontstyle.Bold})),
|
||||
col.New(4).Add(text.New(s.SrcIP, props.Text{Size: 7})),
|
||||
col.New(3).Add(text.New(s.DstIP+" "+humanBytes(s.Bytes), props.Text{Size: 7})),
|
||||
),
|
||||
}
|
||||
func buildTrafficLogRow(s storage.PacketStat, idx int) []core.Row {
|
||||
r := row.New(6).Add(
|
||||
col.New(3).Add(text.New(s.CreatedAt.Format("02.01 15:04:05"), props.Text{Size: 7, Left: 2, Top: 1})),
|
||||
col.New(2).Add(text.New(s.Protocol, props.Text{Size: 7, Style: fontstyle.Bold, Left: 2, Top: 1})),
|
||||
col.New(4).Add(text.New(s.SrcIP, props.Text{Size: 7, Left: 2, Top: 1})),
|
||||
col.New(3).Add(text.New(s.DstIP+" "+humanBytes(s.Bytes), props.Text{Size: 7, Left: 2, Top: 1})),
|
||||
)
|
||||
r.WithStyle(dataRowStyle(idx))
|
||||
return []core.Row{r}
|
||||
}
|
||||
|
||||
// ── Журнал инцидентов ─────────────────────────────────────────────────────────
|
||||
|
||||
func buildAlertsTableHeader() []core.Row {
|
||||
return []core.Row{tableHeaderRow([]colDef{
|
||||
{w: 3, label: "Дата/Время"},
|
||||
{w: 2, label: "Тип"},
|
||||
{w: 2, label: "Критичность"},
|
||||
{w: 2, label: "Источник"},
|
||||
{w: 3, label: "Описание"},
|
||||
{w: 3, label: "Источник"},
|
||||
{w: 2, label: "Описание"},
|
||||
})}
|
||||
}
|
||||
|
||||
func buildAlertRow(a storage.Alert) []core.Row {
|
||||
return []core.Row{
|
||||
row.New(6).Add(
|
||||
col.New(3).Add(text.New(a.CreatedAt.Format("02.01.06 15:04:05"), props.Text{Size: 7})),
|
||||
col.New(2).Add(text.New(typeLabel(a.Type), props.Text{Size: 7})),
|
||||
col.New(2).Add(text.New(severityLabel(a.Severity), props.Text{
|
||||
Size: 7, Style: fontstyle.Bold, Color: severityColor(a.Severity),
|
||||
})),
|
||||
col.New(2).Add(text.New(a.SrcIP, props.Text{Size: 7})),
|
||||
col.New(3).Add(text.New(truncate(a.Description, 60), props.Text{Size: 7})),
|
||||
),
|
||||
}
|
||||
func buildAlertRow(a storage.Alert, idx int) []core.Row {
|
||||
r := row.New(7).Add(
|
||||
col.New(3).Add(text.New(a.CreatedAt.Format("02.01.06 15:04"), props.Text{Size: 7, Left: 2, Top: 2})),
|
||||
col.New(2).Add(text.New(typeLabel(a.Type), props.Text{Size: 7, Left: 2, Top: 2})),
|
||||
col.New(2).Add(text.New(severityLabel(a.Severity), props.Text{
|
||||
Size: 7,
|
||||
Style: fontstyle.Bold,
|
||||
Color: severityColor(a.Severity),
|
||||
Left: 2,
|
||||
Top: 2,
|
||||
})),
|
||||
col.New(3).Add(text.New(a.SrcIP, props.Text{Size: 7, Left: 2, Top: 2})),
|
||||
col.New(2).Add(text.New(truncate(a.Description, 40), props.Text{Size: 7, Left: 2, Top: 2})),
|
||||
)
|
||||
r.WithStyle(dataRowStyle(idx))
|
||||
return []core.Row{r}
|
||||
}
|
||||
|
||||
// ── Вспомогательные типы и функции ───────────────────────────────────────────
|
||||
@@ -360,11 +416,33 @@ func tableHeaderRow(cols []colDef) core.Row {
|
||||
text.New(c.label, props.Text{
|
||||
Size: 9,
|
||||
Style: fontstyle.Bold,
|
||||
Color: &props.Color{Red: 255, Green: 255, Blue: 255},
|
||||
Color: colorWhite,
|
||||
Left: 2,
|
||||
Top: 2,
|
||||
}),
|
||||
))
|
||||
}
|
||||
return row.New(7).Add(cells...)
|
||||
r := row.New(8).Add(cells...)
|
||||
r.WithStyle(&props.Cell{
|
||||
BackgroundColor: colorBlue,
|
||||
BorderColor: colorBlueDark,
|
||||
BorderType: border.Full,
|
||||
BorderThickness: 0.3,
|
||||
})
|
||||
return r
|
||||
}
|
||||
|
||||
func dataRowStyle(idx int) *props.Cell {
|
||||
bg := colorWhite
|
||||
if idx%2 == 0 {
|
||||
bg = colorGray100
|
||||
}
|
||||
return &props.Cell{
|
||||
BackgroundColor: bg,
|
||||
BorderColor: colorGray200,
|
||||
BorderType: border.Bottom,
|
||||
BorderThickness: 0.3,
|
||||
}
|
||||
}
|
||||
|
||||
func severityLabel(s string) string {
|
||||
@@ -433,8 +511,9 @@ func humanBytes(b int64) string {
|
||||
}
|
||||
|
||||
func truncate(s string, n int) string {
|
||||
if len([]rune(s)) <= n {
|
||||
runes := []rune(s)
|
||||
if len(runes) <= n {
|
||||
return s
|
||||
}
|
||||
return string([]rune(s)[:n-3]) + "..."
|
||||
return string(runes[:n-3]) + "..."
|
||||
}
|
||||
|
||||
+14
-14
@@ -87,8 +87,8 @@ func (d *DB) CreatePacketStat(s *PacketStat) error {
|
||||
}
|
||||
|
||||
type SeverityCount struct {
|
||||
Severity string
|
||||
Count int64
|
||||
Severity string `json:"severity"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (d *DB) AlertsBySeverity() ([]SeverityCount, error) {
|
||||
@@ -101,8 +101,8 @@ func (d *DB) AlertsBySeverity() ([]SeverityCount, error) {
|
||||
}
|
||||
|
||||
type TypeCount struct {
|
||||
Type string
|
||||
Count int64
|
||||
Type string `json:"type"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (d *DB) AlertsByType() ([]TypeCount, error) {
|
||||
@@ -115,8 +115,8 @@ func (d *DB) AlertsByType() ([]TypeCount, error) {
|
||||
}
|
||||
|
||||
type IPCount struct {
|
||||
SrcIP string
|
||||
Count int64
|
||||
SrcIP string `json:"src_ip"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (d *DB) TopSourceIPs(limit int) ([]IPCount, error) {
|
||||
@@ -131,16 +131,16 @@ func (d *DB) TopSourceIPs(limit int) ([]IPCount, error) {
|
||||
}
|
||||
|
||||
type TimelinePoint struct {
|
||||
Hour string
|
||||
Count int64
|
||||
Hour string `json:"hour"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
|
||||
func (d *DB) AlertsTimeline(hours int) ([]TimelinePoint, error) {
|
||||
since := time.Now().Add(-time.Duration(hours) * time.Hour)
|
||||
since := time.Now().UTC().Add(-time.Duration(hours) * time.Hour)
|
||||
var result []TimelinePoint
|
||||
err := d.db.Model(&Alert{}).
|
||||
Select("strftime('%Y-%m-%dT%H:00:00', created_at) as hour, count(*) as count").
|
||||
Where("created_at >= ?", since).
|
||||
Select("strftime('%Y-%m-%dT%H:00:00Z', created_at) as hour, count(*) as count").
|
||||
Where("created_at >= datetime(?)", since.Format("2006-01-02 15:04:05")).
|
||||
Group("hour").
|
||||
Order("hour asc").
|
||||
Find(&result).Error
|
||||
@@ -154,9 +154,9 @@ func (d *DB) TotalAlerts() (int64, error) {
|
||||
}
|
||||
|
||||
type ProtocolCount struct {
|
||||
Protocol string
|
||||
Count int64
|
||||
Bytes int64
|
||||
Protocol string `json:"protocol"`
|
||||
Count int64 `json:"count"`
|
||||
Bytes int64 `json:"bytes"`
|
||||
}
|
||||
|
||||
func (d *DB) TrafficByProtocol() ([]ProtocolCount, error) {
|
||||
|
||||
Reference in New Issue
Block a user