148 lines
3.7 KiB
Go
148 lines
3.7 KiB
Go
package analyzer
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"dip-ids/internal/config"
|
|
"dip-ids/internal/storage"
|
|
|
|
"github.com/google/gopacket"
|
|
"github.com/google/gopacket/layers"
|
|
)
|
|
|
|
// dnsAnomalyDetector обнаруживает аномалии DNS:
|
|
// 1. Слишком длинные метки (признак DNS-туннелирования)
|
|
// 2. Аномально высокая частота запросов от одного источника
|
|
type dnsAnomalyDetector struct {
|
|
mu sync.Mutex
|
|
cfg config.DNSAnomalyConfig
|
|
tracker map[string][]time.Time // srcIP -> query timestamps
|
|
alerted map[string]time.Time
|
|
}
|
|
|
|
func NewDNSAnomalyDetector(cfg config.DNSAnomalyConfig) Detector {
|
|
return &dnsAnomalyDetector{
|
|
cfg: cfg,
|
|
tracker: make(map[string][]time.Time),
|
|
alerted: make(map[string]time.Time),
|
|
}
|
|
}
|
|
|
|
func (d *dnsAnomalyDetector) Name() string { return "dns_anomaly" }
|
|
|
|
func (d *dnsAnomalyDetector) Analyze(pkt gopacket.Packet) []storage.Alert {
|
|
if !d.cfg.Enabled {
|
|
return nil
|
|
}
|
|
|
|
netLayer := pkt.NetworkLayer()
|
|
dnsLayer := pkt.Layer(layers.LayerTypeDNS)
|
|
if netLayer == nil || dnsLayer == nil {
|
|
return nil
|
|
}
|
|
|
|
dns := dnsLayer.(*layers.DNS)
|
|
if !dns.QR { // QR=0 — это запрос, QR=1 — ответ; анализируем запросы
|
|
return nil
|
|
}
|
|
|
|
// Анализируем только запросы (QR=false в gopacket означает запрос)
|
|
if len(dns.Questions) == 0 {
|
|
return nil
|
|
}
|
|
|
|
srcIP := netLayer.NetworkFlow().Src().String()
|
|
now := pkt.Metadata().Timestamp
|
|
if now.IsZero() {
|
|
now = time.Now()
|
|
}
|
|
|
|
var alerts []storage.Alert
|
|
|
|
for _, q := range dns.Questions {
|
|
name := string(q.Name)
|
|
|
|
// Проверка 1: DNS-туннелирование по длине метки
|
|
if alert := d.checkTunneling(name, srcIP, now); alert != nil {
|
|
alerts = append(alerts, *alert)
|
|
}
|
|
}
|
|
|
|
// Проверка 2: высокая частота запросов
|
|
if alert := d.checkFrequency(srcIP, now); alert != nil {
|
|
alerts = append(alerts, *alert)
|
|
}
|
|
|
|
return alerts
|
|
}
|
|
|
|
func (d *dnsAnomalyDetector) checkTunneling(name, srcIP string, now time.Time) *storage.Alert {
|
|
labels := strings.Split(name, ".")
|
|
for _, label := range labels {
|
|
if len(label) > d.cfg.MaxLabelLength {
|
|
key := "tunnel:" + srcIP
|
|
d.mu.Lock()
|
|
last, exists := d.alerted[key]
|
|
if exists && now.Sub(last) < 60*time.Second {
|
|
d.mu.Unlock()
|
|
return nil
|
|
}
|
|
d.alerted[key] = now
|
|
d.mu.Unlock()
|
|
|
|
return &storage.Alert{
|
|
CreatedAt: now,
|
|
Type: "dns_anomaly",
|
|
Severity: d.cfg.Severity,
|
|
SrcIP: srcIP,
|
|
Protocol: "DNS",
|
|
Description: fmt.Sprintf("DNS tunneling suspected: label length %d in query '%s'", len(label), name),
|
|
Count: 1,
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (d *dnsAnomalyDetector) checkFrequency(srcIP string, now time.Time) *storage.Alert {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
window := time.Duration(d.cfg.WindowSeconds) * time.Second
|
|
cutoff := now.Add(-window)
|
|
|
|
d.tracker[srcIP] = append(d.tracker[srcIP], now)
|
|
d.tracker[srcIP] = filterTimes(d.tracker[srcIP], cutoff)
|
|
|
|
count := len(d.tracker[srcIP])
|
|
if count < d.cfg.QueryThreshold {
|
|
return nil
|
|
}
|
|
|
|
key := "freq:" + srcIP
|
|
if last, ok := d.alerted[key]; ok && now.Sub(last) < 30*time.Second {
|
|
return nil
|
|
}
|
|
d.alerted[key] = now
|
|
|
|
return &storage.Alert{
|
|
CreatedAt: now,
|
|
Type: "dns_anomaly",
|
|
Severity: d.cfg.Severity,
|
|
SrcIP: srcIP,
|
|
Protocol: "DNS",
|
|
Description: fmt.Sprintf("High DNS query rate from %s: %d queries in %ds", srcIP, count, d.cfg.WindowSeconds),
|
|
Count: count,
|
|
}
|
|
}
|
|
|
|
func (d *dnsAnomalyDetector) Reset() {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
d.tracker = make(map[string][]time.Time)
|
|
d.alerted = make(map[string]time.Time)
|
|
}
|