Files
dip-ids/internal/analyzer/portscan.go
T
2026-04-12 22:26:26 +04:00

122 lines
3.1 KiB
Go

package analyzer
import (
"fmt"
"sync"
"time"
"dip-ids/internal/config"
"dip-ids/internal/storage"
"github.com/google/gopacket"
"github.com/google/gopacket/layers"
)
// portScanDetector обнаруживает сканирование портов методом sliding window.
// Алгоритм: если один источник обращается к N уникальным портам назначения
// за период T — это потенциальное сканирование.
type portScanDetector struct {
mu sync.Mutex
cfg config.PortScanConfig
// srcIP -> map[dstPort][]time.Time
tracker map[string]map[uint16][]time.Time
alerted map[string]time.Time // последний алерт по srcIP
}
func NewPortScanDetector(cfg config.PortScanConfig) Detector {
return &portScanDetector{
cfg: cfg,
tracker: make(map[string]map[uint16][]time.Time),
alerted: make(map[string]time.Time),
}
}
func (d *portScanDetector) Name() string { return "port_scan" }
func (d *portScanDetector) Analyze(pkt gopacket.Packet) []storage.Alert {
if !d.cfg.Enabled {
return nil
}
netLayer := pkt.NetworkLayer()
tcpLayer := pkt.Layer(layers.LayerTypeTCP)
if netLayer == nil || tcpLayer == nil {
return nil
}
tcp := tcpLayer.(*layers.TCP)
// Интересуют только SYN-пакеты (начало соединения)
if !tcp.SYN || tcp.ACK {
return nil
}
srcIP := netLayer.NetworkFlow().Src().String()
dstIP := netLayer.NetworkFlow().Dst().String()
dstPort := uint16(tcp.DstPort)
now := pkt.Metadata().Timestamp
if now.IsZero() {
now = time.Now()
}
d.mu.Lock()
defer d.mu.Unlock()
if _, ok := d.tracker[srcIP]; !ok {
d.tracker[srcIP] = make(map[uint16][]time.Time)
}
window := time.Duration(d.cfg.WindowSeconds) * time.Second
cutoff := now.Add(-window)
// Добавляем текущий порт и очищаем устаревшие записи
d.tracker[srcIP][dstPort] = append(d.tracker[srcIP][dstPort], now)
uniquePorts := 0
for port, times := range d.tracker[srcIP] {
filtered := filterTimes(times, cutoff)
if len(filtered) == 0 {
delete(d.tracker[srcIP], port)
} else {
d.tracker[srcIP][port] = filtered
uniquePorts++
}
}
if uniquePorts < d.cfg.Threshold {
return nil
}
// Дедупликация: не спамить алертами чаще чем раз в 30 сек на один IP
if last, ok := d.alerted[srcIP]; ok && now.Sub(last) < 30*time.Second {
return nil
}
d.alerted[srcIP] = now
return []storage.Alert{{
CreatedAt: now,
Type: "port_scan",
Severity: d.cfg.Severity,
SrcIP: srcIP,
DstIP: dstIP,
DstPort: dstPort,
Protocol: "TCP",
Description: fmt.Sprintf("Port scan detected: %d unique ports probed in %ds window", uniquePorts, d.cfg.WindowSeconds),
Count: uniquePorts,
}}
}
func (d *portScanDetector) Reset() {
d.mu.Lock()
defer d.mu.Unlock()
d.tracker = make(map[string]map[uint16][]time.Time)
d.alerted = make(map[string]time.Time)
}
func filterTimes(times []time.Time, cutoff time.Time) []time.Time {
i := 0
for i < len(times) && times[i].Before(cutoff) {
i++
}
return times[i:]
}