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
+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) {