feat: healthcheck backend and fix data types

also small various features
This commit is contained in:
kilyabin
2026-04-28 00:10:06 +04:00
parent 1cb93a1f31
commit 78e5c90ed0
9 changed files with 305 additions and 132 deletions
+46 -4
View File
@@ -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) {
+24
View File
@@ -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 }
}
+21 -2
View File
@@ -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)
+4 -3
View File
@@ -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/**' },
},
+7 -4
View File
@@ -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,
}],
}
+6 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
BIN
View File
Binary file not shown.